Linux网络——套接字编程

news2024/11/20 21:54:20

目录

1. 网络通信基本脉络

2. 端口号

① 什么是套接字编程?

② 端口号 port && 进程 PID

3. 网络字节序

4. 套接字编程

① UDP版

② TCP版

5. 改进方案与拓展

①多进程版

②多线程版

③线程池版

④守护进程化

1. 简单的重联

2. session && 前台 && 后台

3. Linux 系统进程间关系

4. 进程的守护进程化


1. 网络通信基本脉络

基本脉络图如上,其中数据在不同层的叫法不一样,比如在传输层时称为数据段,而在网络层时称为数据报。我们可以在 Linux 中使用 ifconfig 查看网络的配置,如图

其中,inet 表示的是 IPv4,inet6 表示的是 IPv6,ehther(以太)表示的是 mac 地址。

2. 端口号

在进行网络通信时,是不是两台机器直接在进行通信呢?——当然不是

1. 网络协议中的下三层,主要解决的是数据能安全可靠的传输到另一台主机上

2. 这之后,用户使用应用软件完成数据的发送和接收

而一个应用软件会被操作系统解释成进程,也就是说网络通信的本质就是进程间通信!那么一个数据被 A 主机传输到 B 主机上后,怎么交给应用层呢?——端口号!端口号对于主机 A 和主机 B 都能唯一标识该主机上的一个网络应用程序的进程。

① 什么是套接字编程?

在公网上, ip 地址能标识唯一一台主机,端口号 port 能标识该主机上的唯一一个进程,因此

 我们可以使用 ip:port 来表示全网唯一的一个进程

而我们将 client_ip:client_port 与 server_ip:server_port 间的通信称为套接字编程!

② 端口号 port && 进程 PID

既然 PID 已经能够标识一台主机上的唯一性了,那为什么我们还需要端口号这个概念呢?

1. 并非所有的进程都需要进行网络通信,但是所有的进程都有 PID

2. 使系统和网络的功能解耦

我们举个例子

假如现在你正在手机上使用抖音,想浏览一个视频,你的手机(客户端)就会将“想浏览一个视频”这个行为发送到服务端, 在发送的时候其会在自己的数据中附带上自己的端口号与服务端的端口号(每一个服务端的端口号必须是众所周知,精心设计,被用户端熟知的),而服务端在接收到这个消息后会按照 IP + port 的形式返回应答!

根据我们对端口号的了解

一个进程是可以绑定多个端口号的!但是一个端口号不能被多个进程绑定! 

3. 网络字节序

我们知道在计算机中是存在大端与小端的,我们将低地址放在低位称为小端,而在 TCP/IP 协议中规定了采用大端字节序,我们可以使用 htonl 接口来转换(h: host 主机,n: net 网络,l: long 4字节),与其类似的还有 ntohl, htons(s: short), ntohs等。

4. 套接字编程

我们先来看看 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);

socket API 是一层抽象的网络编程接口,适用于各种底层网络协议,如 IPv4、IPv6,以及 UNIX Domain Socket。但是,各种网络协议的地址格式并不是相同的。套接字编程也分为几种

1. 域间套接字编程 -> 同一机器内

2. 原始套接字编程 -> 网络工具

3. 网络套接字编程 -> 用户间的网络通信

我们想将网络接口统一抽象化,那就表示着参数类型必须是统一的,比如对于这个 struct sockaddr* address 来说,其设计如下

我们在设计接口时,将其设计为基类 struct sockaddr* address ,在使用时我们根据需要传入其对应的子类,这实际上使用到了面向对象中的多态思想!

① UDP版

接下来我们就简单完成一个 UDP 版本的套接字,其模板如下

#pragma once

#include <iostream>
#include <sys/types.h>
#include <sys/socket.h>

#include "Log.hpp"
extern Log lg;

enum 
{
    SOCKET_ERR=1;
};

class UdpServer
{
public:
    UdpServer()
    {}
    
    void Init()
    {
        sockfd_ = socket(AF_INET, SOCK_DGRAM, 0);
        if (sockfd_ < 0)
        {
            lg(Fatal, "socket create error, sockfd: %d" , sockfd_);
            exit(SOCKET_ERR);
        }
    }

    void Run()
    {}

    ~UdpServer()
    {}
private:
    int sockfd_; // 网路文件描述符
};

其调用逻辑如下

#include "Udpserver.hpp"
#include <memory>

int main()
{
    std::unique_ptr<UdpServer> svr(new UdpServer());
    
    svr->Init(/**/);
    svr->Run();

    return 0;
}

 我们来看看 socket  这个接口

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

int 表示返回一个文件描述符,int domain 表示要在什么域进行传输(这里我们选择的是 AF_INET-> IPv4),int type 表示创建什么类型的套接字(这里我们选择的是 SOCK_DGRAM ,即 Supports datagrams 面向数据报,也就是 udp 使用的类型),而 int protocol 表示使用什么协议类型。 

接下来我们来完成这个套接字

#pragma once

#include <iostream>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <cstring>
#include <functional>

#include "Log.hpp"
extern Log lg;

typedef std::function<std::string(const std::string&)> func_t;

// 枚举错误信息
enum 
{
    SOCKET_ERR=1,
    BIND_ERR
};

// 服务器默认端口号
uint16_t defaultport = 8081;
// 服务器默认ip
std::string defaultip = "0.0.0.0";

// 数据缓冲区大小
const int size = 1024;

class UdpServer
{
public:
    UdpServer(const uint16_t &port = defaultport, const std::string &ip = defaultip)
        :port_(port), ip_(ip), isrunning_(false)
    {}
    
    void Init()
    {
        // 1. 创建 udp socket
        sockfd_ = socket(AF_INET, SOCK_DGRAM, 0);
        if (sockfd_ < 0)
        {
            lg(Fatal, "socket create error, sockfd: %d" , sockfd_);
            exit(SOCKET_ERR);
        }
        lg(Info, "socket create success, sockfd: %d" , sockfd_);
        
        // 2. bind socket

        // 初始化 local
        struct sockaddr_in local;
        bzero(&local, sizeof(local)); // 清零 local
        local.sin_family = AF_INET; // 设置为 IPv4
        local.sin_port = htons(port_); // 需要保证这里的端口号是网络字节序列,因为该端口号是要给对方发送的
        // 1. string -> uint32_t 
        // 2. uint32_t必须是网络序列的
        local.sin_addr.s_addr = inet_addr(ip_.c_str()); 

        if(bind(sockfd_, (const struct sockaddr*)&local, sizeof(local)) < 0)
        {
            lg(Fatal,"bind error, errno: %d, err string: %s" , errno, strerror(errno));
            exit(BIND_ERR);
        }
        lg(Info, "bind socket success!");
    }

    void Run(func_t func)
    {
        // 设置服务器运行状态为 运行中
        isrunning_ = true;
        
        // 设置缓冲区
        char inbuffer[size];
        while(isrunning_)
        {
            // 输出型参数 client
            struct sockaddr_in client;
            socklen_t len = sizeof(client);

            // 从 inbuffer 中读取数据
            // sizeof(inbuffer)-1 意思是将 inbufffer 视为字符串
            // n 表示实际接收到了多少个字符
            // 同时获取 client 信息,便于之后的发送
            ssize_t n = recvfrom(sockfd_, inbuffer, sizeof(inbuffer)-1, 0, (struct sockaddr*)&client, &len);

            if(n < 0)
            {
                lg(Warning, "recvfrom error, errno: %d, err string: %s", errno, strerror(errno));
                continue;
            }
            inbuffer[n]= 0;

            //充当了一次数据的处理
            std::string info = inbuffer;
            std::string echo_string = func(info);

            // server 向 client 发送应答信息
            sendto(sockfd_, echo_string.c_str(), echo_string.size(), 0, (struct sockaddr*)&client, len);
        }
    }

    ~UdpServer()
    {
        if (sockfd_ > 0) close(sockfd_);
    }
private:
    int sockfd_;       // 网路文件描述符
    std::string ip_;   // 服务器ip
    uint16_t port_;    // 服务器进程的端口号
    bool isrunning_;   // 服务器运行状态
};

接下来完成 udpserver 的编写

#include "UdpServer.hpp"
#include <memory>

void Usage(std::string proc)
{
    std::cout << "\n\rUsage: " << proc << " port[1024+]\n" << std::endl;
}

std::string Handler(const std::string &str)
{
    std::string res = "Server get a message: ";
    res += str;
    std::cout << res << std::endl;

    return res;
}

std::string ExcuteCommand(const std::string &cmd)
{
    FILE *fp = popen(cmd.c_str(), "r");
    if(nullptr == fp)
    {
        perror("popen");
        return "error";
    }
    std::string result;
    char buffer[4096];
    while(true)
    {
        char *ok = fgets(buffer, sizeof(buffer), fp);
        if(ok == nullptr) break;
        result += buffer;
    }
    pclose(fp);

    return result;
}

// ./udpserver port
int main(int argc, char *argv[])
{
    if(argc != 2)
    {
        Usage(argv[0]);
        exit(0);
    }

    uint16_t port = std::stoi(argv[1]);
    std::unique_ptr<UdpServer> svr(new UdpServer(port));
    
    svr->Init();
    svr->Run(ExcuteCommand);

    return 0;
}

 接下来我们编写一个 udpclient 来与其进行通信

#include "UdpServer.hpp"
#include <memory>
#include <iostream>

using namespace std;

extern Log lg;

void Usage(std::string proc)
{
    std::cout << "\n\rUsage: " << proc << " serverip serverport\n" << std::endl;
}

// ./udpclient serverip serverport
int main(int argc, char *argv[])
{
    if(argc != 3)
    {
        Usage(argv[0]);
        exit(0);
    }
    std::string serverip = argv[1];
    uint16_t serverport = std::stoi(argv[2]);

    // 确认发送服务端
    struct sockaddr_in server;
    bzero(&server, sizeof(server));
    server.sin_family = AF_INET;
    server.sin_port = htons(serverport);
    server.sin_addr.s_addr = inet_addr(serverip.c_str());
    socklen_t len = sizeof(server);

    int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    if(sockfd < 0)
    {
        cout << "socker error" << endl;
        return 1;
    }
    lg(Info, "socket create success, sockfd: %d", sockfd);
    // client要bind吗?——要,只不过不需要用户显示的bind!一般由 OS 自由随机选择!
    //一个端口号只能被一个进程bind,对server是如此,对于client,也是如此!
    //其实client的port是多少,其实不重要,只要能保证主机上的唯一性就可以!
    //系统什么时候bind呢?——首次发送数据的时候

    string message;
    char buffer[1024];
    while(true)
    {
        // 1. 获取数据
        cout << "Please Enter: ";
        getline(cin, message);
        
        // 2. 给服务端发送信息
        sendto(sockfd, message.c_str(), message.size(), 0, (struct sockaddr*)&server, len);

        struct sockaddr_in tmp;
        socklen_t t_len = sizeof(tmp);

        ssize_t n = recvfrom(sockfd, buffer, 1023, 0, (struct sockaddr*)&tmp, &t_len);
        if (n > 0)
        {
            buffer[n] = 0;
            cout << buffer << endl;
        }
    }

    close(sockfd);
    return 0;
}

运行效果如下

② TCP版

我们先来看看 TCP 方案的模板

#pragma once

#include <iostream>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <cstring>
#include <functional>

#include "Log.hpp"
extern Log lg;

// 枚举错误信息
enum 
{
    SOCKET_ERR=1,
    BIND_ERR
};

// 服务器默认端口号
uint16_t defaultport = 8081;
// 服务器默认ip
std::string defaultip = "0.0.0.0";

class TcpServer
{
public:
    TcpServer(const uint16_t &port = defaultport, const std::string &ip = defaultip) 
        :port_(port), ip_(ip)
    {}

    void InitServer()
    {
        // 创建套接字为 IPv4, 字节流(TCP)
        sockfd_ = socket(AF_INET, SOCK_STREAM, 0);
        if(sockfd_ < 0)
        {
            lg(Fatal, "create socket, errno: %d, errstring: %s", errno, strerror(errno));
            exit(SOCKET_ERR);
        }
        lg(Info, "create socket success, sockfd: %d" , sockfd_);
        
        // 初始化 local
        struct sockaddr_in local;
        memset(&local, 0, sizeof(local));
        local.sin_family = AF_INET;   // 设置为 IPv4
        local.sin_port = htons(port_);// 保证端口号是网络字节序列
        // 将 IPv4 的字符串转化成 in_addr 并返回字符串的起始地址
        // 返回的字符串存储在静态区(多次调用只保存最后一次调用结果) 
        inet_aton(ip_.c_str(), &(local.sin_addr));

        // 绑定套接字
        if(bind(sockfd_, (struct sockaddr*)&local, sizeof(local)) < 0)
        {
            lg(Fatal,"bind error,errno: %d,errstring: %s" , errno, strerror(errno));
            exit(BIND_ERR);
        }
        lg(Info, "bind socket success!" , sockfd_);
    }

    void Start(){}
    
    ~TcpServer(){}
private:
    int sockfd_;       // 网路文件描述符
    std::string ip_;   // 服务器ip
    uint16_t port_;    // 服务器进程的端口号
    bool isrunning_;   // 服务器运行状态
};

我们完成它的代码,如下

#pragma once

#include <iostream>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <cstring>
#include <functional>

#include "Log.hpp"
extern Log lg;

// 枚举错误信息
enum 
{
    SOCKET_ERR=1,
    BIND_ERR,
    LISTEN_ERR
};

// 服务器默认端口号
uint16_t defaultport = 8081;

// 服务器默认ip
std::string defaultip = "0.0.0.0";

// 监听接口 listen 的第二个参数
// listen 函数的第二个参数 backlog 表示等待队列的最大长度。这个等待队列是用于存放那些已经到达但还没有被 accept 函数接受的连接请求。
// 当一个新的连接请求到达时,如果服务器的等待队列还没有满,那么这个连接请求就会被添加到队列中,等待服务器的 accept 函数来处理。
// 如果等待队列已经满了,那么新的连接请求可能就会被拒绝,客户端可能会收到一个 ECONNREFUSED 错误。
const int backlog = 10;

class TcpServer
{
public:
    TcpServer(const uint16_t &port = defaultport, const std::string &ip = defaultip) 
        :port_(port), ip_(ip)
    {}

    void InitServer()
    {
        // 创建套接字为 IPv4, 字节流(TCP)
        listensock_ = socket(AF_INET, SOCK_STREAM, 0);
        if(listensock_ < 0)
        {
            lg(Fatal, "create listensock error, errno: %d, errstring: %s", errno, strerror(errno));
            exit(SOCKET_ERR);
        }
        lg(Info, "create listensock success, listensock: %d" , listensock_);
        
        // 初始化 local
        struct sockaddr_in local;
        memset(&local, 0, sizeof(local));
        local.sin_family = AF_INET;   // 设置为 IPv4
        local.sin_port = htons(port_);// 保证端口号是网络字节序列
        // 将 IPv4 的字符串转化成 in_addr 并返回字符串的起始地址
        // 返回的字符串存储在静态区(多次调用只保存最后一次调用结果) 
        inet_aton(ip_.c_str(), &(local.sin_addr));

        // 当 local.sin_addr.s_addr 被设置为 INADDR_ANY 时
        // 意思是告诉操作系统,我们希望绑定的套接字监听所有可用的网络接口上的指定端口。
        // 这样设置后,当有数据包到达端口时,无论它们来自哪个网络接口,套接字都能接收到。
        // 在服务器编程中,这通常用于监听所有网络接口上的连接请求,而不是只监听某个特定的 IP 地址。
        // 这样,服务器可以接受来自任何网络接口的连接,而不仅仅是一个特定的接口。
        local.sin_addr.s_addr = INADDR_ANY;

        // 绑定套接字
        if(bind(listensock_, (struct sockaddr*)&local, sizeof(local)) < 0)
        {
            lg(Fatal,"bind error,errno: %d,errstring: %s" , errno, strerror(errno));
            exit(BIND_ERR);
        }
        lg(Info, "bind socket success, sockfd: %d" , listensock_);

        // tcp是面向连接的,服务器一般是比较“被动的”,服务器一直处于一种,一直在等待连接到来的状态
        // 监听套接字
        if (listen(listensock_, backlog) < 0)
        {
            lg(Fatal, "listen error, errno: %d, errstring: %s ", errno, strerror(errno));
            exit(LISTEN_ERR);
        }

        lg(Info, "listen socket success, sockfd: %d" , listensock_);
    }

    // 启动服务器
    void Start()
    {
        lg(Info, "TCP server is running...");
        for (;;)
        {
            //1.获取新连接
            struct sockaddr_in client;
            socklen_t len = sizeof(client);

            // accept 类似于 recvfrom 
            // 其返回一个文件描述符,后两个参数表示获取哪个用户的信息
            int sockfd = accept(listensock_, (struct sockaddr *)&client, &len);

            // sockfd && listensock_
            // 举个简单的例子,对于一个农家乐来说会存在两种人
            // 一种是去拉客到农家乐内,另一种是在农家乐内进行服务的
            // listensock_ -> 拉客的人; sockfd -> 进行服务的人
            // listensock_ 只负责监听,如果监听失败会等待监听下一个主机
            // sockfd 只负责通信,其可能会变得越来越多
            if (sockfd < 0)
            {
                lg(Fatal, "listen erron,errno: %d, errstring: %s" , errno, strerror(errno));
                continue;
            }
            
            uint16_t clientport = ntohs(client.sin_port);
            char clientip[32];
            // inet_ntop 将网络地址转换成文本表示形式
            // 它是 inet_aton 的逆函数,它将一个网络地址(通常是 IP 地址)从二进制形式转换为人类可读的字符串形式。
            // AF_INET 表示 IPv4 地址; &(client.sin_addr) 指向要转换的网络地址; clientip 是存储转换结果的字符串缓冲区; sizeof(clientip) 是 ipstr 缓冲区的大小
            // 确保 inet_ntop 不会写入超出缓冲区范围的内存。
            inet_ntop(AF_INET,&(client.sin_addr), clientip, sizeof(clientip));

            // 2.根据新连接进行通信
            lg(Info, "get a new link, sockfd: %d, client ip: %s, client port: %d", sockfd, clientip, clientport);
            Service(sockfd, clientip, clientport);
        }
    }
    
    void Service(int sockfd, const std::string &clientip, const uint16_t &clientport)
    {
        //测试代码
        char buffer[4096];
        while(true)
        {
            ssize_t n = read(sockfd, buffer, sizeof(buffer));
            if(n > 0)
            {
                buffer[n] = 0;
                std::cout << "client say: " << buffer << std::endl;
                std::string echo_string = "tcpserver echo: ";
                echo_string += buffer;

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


    ~TcpServer()
    {
        if (listensock_ > 0) close(listensock_);
    }
private:
    int listensock_;   // 监听套接字
    std::string ip_;   // 服务器ip
    uint16_t port_;    // 服务器进程的端口号
    bool isrunning_;   // 服务器运行状态
};

其调用逻辑如下

#include "TcpServer.hpp"
#include <memory>

void Usage(std::string proc)
{
    std::cout << "\n\rUsage: " << proc << " port[1024+]\n" << std::endl;
}

// ./tcpserver port
int main(int argc, char *argv[])
{
    if(argc != 2)
    {
        Usage(argv[0]);
        exit(0);
    }

    uint16_t port = std::stoi(argv[1]);
    std::unique_ptr<TcpServer> svr(new TcpServer(port));
    
    svr->InitServer();
    svr->Start();

    return 0;
}

 我们可以使用 telnet 来对其进行测试,如图

接下来我们编写一个 client 客户端进行测试,代码如下

#include "TcpServer.hpp"
#include <memory>
#include <iostream>

using namespace std;

extern Log lg;

void Usage(std::string proc)
{
    std::cout << "\n\rUsage: " << proc << " serverip serverport\n" << std::endl;
}

// ./tcpclient serverip serverport
int main(int argc, char *argv[])
{
    if(argc != 3)
    {
        Usage(argv[0]);
        exit(0);
    }
    std::string serverip = argv[1];
    uint16_t serverport = std::stoi(argv[2]);

    int sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if(sockfd < 0)
    {
        cout << "socker error" << endl;
        return 1;
    }
    lg(Info, "socket create success, sockfd: %d", sockfd);

    struct sockaddr_in server;
    memset(&server, 0, sizeof(server));
    server.sin_family = AF_INET;
    server.sin_port = htons(serverport);
    // inet_pton 中 p -> process, n -> net 意为将本地的东西转换为网络的东西
    inet_pton(AF_INET, serverip.c_str(), &(server.sin_addr));

    //tcp客户端要不要bind?——要的,那要不要显示的bind?——不需要,系统使用随机端口进行bind
    //客户端发起connect的时候,进行自动随机bind

    // connect 类似于sendto
    int n = connect(sockfd, (struct sockaddr*)&server, sizeof(server));
    if(n < 0)
    {
        std::cerr << "connect error. . ." << std::endl;
        return 2;
    }

    std::string message;
    while(true)
    {
        std::cout << "Please Enter: ";
        std::getline(std::cin, message);

        write(sockfd, message.c_str(), message.size());
        
        char inbuffer[4096];
        int n = read(sockfd, inbuffer, sizeof(inbuffer));
        if(n > 0)
        {
            inbuffer[n] = 0;
            std::cout << inbuffer << std::endl;
        }
    }

    close(sockfd);
    return 0;
}

测试效果如下

5. 改进方案与拓展

①多进程版

我们修改 Start 函数,即

void Start()
{
    lg(Info, "TCP server is running...");
    for (;;)
    {
        struct sockaddr_in client;
        socklen_t len = sizeof(client);

        int sockfd = accept(listensock_, (struct sockaddr *)&client, &len);
        if (sockfd < 0)
        {
            lg(Fatal, "listen erron,errno: %d, errstring: %s" , errno, strerror(errno));
            continue;
        }
        
        uint16_t clientport = ntohs(client.sin_port);
        char clientip[32];
        inet_ntop(AF_INET,&(client.sin_addr), clientip, sizeof(clientip));

        lg(Info, "get a new link, sockfd: %d, client ip: %s, client port: %d", sockfd, clientip, clientport);
        
        // 多进程服务
        pid_t id = fork();
        if(id == 0)
        {
            // child
            close(listensock_);
            
            if (fork() > 0) exit(0);
            // 使用孙子进程服务,由 system 领养
            // 从而使孙子进程与父进程并发执行
            Service(sockfd, clientip, clientport);
            
            close(sockfd);
            exit(0);
        }

        // father
        pid_t rid = waitpid(id, nullptr, 0);
        (void)rid;
    }
}

此外,我们也可以在最开始设置

signal(SIGCHID, IGN);

来提升并发度,但是这种方案的成本太高了,所以我们一般不推荐这种做法。 

②多线程版

我们稍作修改,有

class ThreadData
{
public:
    ThreadData(int fd, const std::string &ip, const uint16_t &p, TcpServer *t)
        :sockfd(fd), clientip(ip), clientport(p), tsvr(t)
    {}
public:
    int sockfd;
    std::string clientip;
    uint16_t clientport;
    TcpServer *tsvr; // static routine 无法访问类内成员,因此需要一个 server 指针
};

void Start()
{
    lg(Info, "TCP server is running...");
    for (;;)
    {
        struct sockaddr_in client;
        socklen_t len = sizeof(client);

        int sockfd = accept(listensock_, (struct sockaddr *)&client, &len);
        if (sockfd < 0)
        {
            lg(Fatal, "listen erron,errno: %d, errstring: %s" , errno, strerror(errno));
            continue;
        }
        
        uint16_t clientport = ntohs(client.sin_port);
        char clientip[32];
        inet_ntop(AF_INET,&(client.sin_addr), clientip, sizeof(clientip));

        // 多线程版
        ThreadData *td = new ThreadData(sockfd, clientip, clientport);
        pthread_t tid;
        pthread_create(&tid, nullptr, Routine, td);
    }
}

static void *Routine(void *args)
{
    pthread_detach(pthread_self());
    ThreadData *td = static_cast<ThreadData*>(args);
    td->tsvr->Service(td->sockfd, td->clientip, td->clientport);
    delete td;
    return nullptr;
}

这种方案已经能大大提升运行效率,我们还可以对其进行优化——使用线程池! 

③线程池版

修改方案如下

#include <iostream>
#include <string>
#include "Log.hpp"
extern Log lg;


class Task
{
public:
    Task(int sockfd, const std::string &clientip, const uint16_t &clientport)
        : sockfd_(sockfd),clientip_(clientip),clientport_(clientport)
    {}

    Task(){}   

    void run()
    {
        //测试代码
        while (true)
        {
            char buffer[4096];
            ssize_t n = read(sockfd_, buffer, sizeof(buffer));
            if(n > 0)
            {
                buffer[n] = 0;

                std::string buff = buffer;
                if (buff == "Bye") break;

                std::cout << "client say: " << buffer << std::endl;
                std::string echo_string = "tcpserver echo: ";
                echo_string += buffer;

                write(sockfd_, echo_string.c_str(), echo_string.size());
            }
            else if (n == 0)
            {
                lg(Info, "%s:%d quit, server close sockfd: %d", clientip_.c_str(), clientport_, sockfd_);
            }
            else
            {
                lg(Warning, "read error, sockfd: %d, client ip: %s, client port: %d", sockfd_, clientip_.c_str(), clientport_);
            }
        }

        lg(Info, "client sockfd is closed, sockfd: %d", sockfd_);
        close(sockfd_);
    }
    
    void operator()()
    {
        run();
    }
    
    ~Task()
    {}
private:
    int sockfd_;
    std::string clientip_;
    uint16_t clientport_;
};


void Start()
{
    lg(Info, "TCP server is running...");
    ThreadPool<Task>::GetInstance()->Start();
    for (;;)
    {
        struct sockaddr_in client;
        socklen_t len = sizeof(client);

        int sockfd = accept(listensock_, (struct sockaddr *)&client, &len);
        if (sockfd < 0)
        {
            lg(Fatal, "listen erron,errno: %d, errstring: %s" , errno, strerror(errno));
            continue;
        }
        
        uint16_t clientport = ntohs(client.sin_port);
        char clientip[32];
        inet_ntop(AF_INET,&(client.sin_addr), clientip, sizeof(clientip));
        lg(Info, "TCP server get a new link, clientip: %s, clientport: %d", clientip, clientport);

        // 线程池版
        Task t(sockfd, clientip, clientport);
        ThreadPool<Task>::GetInstance()->Push(t);
    }
}

运行效果如下

④守护进程化

1. 简单的重联

在实际的连接过程中我们可能会出现各种各样的问题,比如网络突然断了,或者服务器在一瞬间突然断开了和客户端的连接,此时我们需要有一种简单的重联方案,比如在游戏中断联就会出现当前正在重新连接,请稍等,接下来我们就简单实现一下这个功能

#include "TcpServer.hpp"
#include <memory>
#include <iostream>

using namespace std;

extern Log lg;

void Usage(std::string proc)
{
    std::cout << "\n\rUsage: " << proc << " serverip serverport\n" << std::endl;
}

// ./tcpclient serverip serverport
int main(int argc, char *argv[])
{
    if(argc != 3)
    {
        Usage(argv[0]);
        exit(0);
    }
    std::string serverip = argv[1];
    uint16_t serverport = std::stoi(argv[2]);

    struct sockaddr_in server;
    memset(&server, 0, sizeof(server));
    server.sin_family = AF_INET;
    server.sin_port = htons(serverport);
    // inet_pton 中 p -> process, n -> net 意为将本地的东西转换为网络的东西
    inet_pton(AF_INET, serverip.c_str(), &(server.sin_addr));

    
    while (true)
    {
        // 尝试进行5次重连
        int cnt = 5;
        int isreconnect = false;
        int sockfd = 0;
        do
        {
            sockfd = socket(AF_INET, SOCK_STREAM, 0);
            if(sockfd < 0)
            {
                cout << "socker error" << endl;
                return 1;
            }   

            //tcp客户端要不要bind?——要的,那要不要显示的bind?——不需要,系统使用随机端口进行bind
            //客户端发起connect的时候,进行自动随机bind

            int n = connect(sockfd, (struct sockaddr*)&server, sizeof(server));
            if(n < 0)
            {
                isreconnect = true;
                cnt--;
                std::cerr << "connect error, reconnecting... times: " << 5 - cnt << std::endl;
                sleep(1);
            }
            else
            {
                break;
            }
        } while (cnt && isreconnect);

        if (cnt == 0)
        {
            cout << "user offline..." << endl;
            break;
        }

        std::string message;
        std::cout << "Please Enter: ";
        std::getline(std::cin, message);

        int n = write(sockfd, message.c_str(), message.size());
        if (n < 0)
        {
            cout << "write error" << endl;
            continue;
        }

        char inbuffer[4096];
        n = read(sockfd, inbuffer, sizeof(inbuffer));
        if(n > 0)
        {
            inbuffer[n] = 0;
            std::cout << inbuffer << std::endl;
        }

        close(sockfd);
    }
    return 0;
}

运行效果如下

2. session && 前台 && 后台

我们通过画图来理解,如图

如图,每当有一个用户登录时,OS 会为其分配一个会话(session),一个 session 只能有前台进程运行,且键盘信号只能发给前台进程。那我们如何分别前台与后台呢?——谁拥有键盘就叫前台!在命令行中,前台会一直存在;前台与后台都能向显示器打印数据,但是后台是不能从标准输入获取数据的。

我们可以在运行程序时在最后带上一个 & 使其在后台运行,举个例子

#include <iostream>

using namespace std;

int main()
{
    while (true)
    {
        cout << "hello world" << endl;
    }

    return 0;
}

运行效果如下

对于 [1] 2744 ,[1] 表示后台任务号,我们可以使用一系列操作来操作它们

jobs -> 查看所有后台任务

fg -n -> 将 n 号任务提到前台

ctrl+z -> 将前台进程放到后台(暂停)

bg -n -> 将后台暂停的进程继续执行 

3. Linux 系统进程间关系

我们在后台多运行几个 test 有

可以看到,多个任务(进程组)在同一个 session 内启动。那进程组和任务间有什么关系呢?

任务的完成往往需要多个进程协同工作,而这些进程可以被组织在一个或多个进程组中。例如,一个复杂的任务可能需要多个进程组来共同完成,每个进程组负责任务的不同部分。在这种情况下,进程组作为任务的一个执行单元,可以被看作是任务的一个子集或实现部分。

那当用户退出的时候后台进程会怎样呢?——会被 OS 领养,即成为孤儿进程,也就是说后台进程受到了用户登录和退出的影响!那我们将不想受到任何用户登录和注销影响的行为称为守护进程化! 

4. 进程的守护进程化

那我们如何做到守护进程化呢?我们可以封装一个接口,即

#include <sys/stat.h>
#include <fcntl.h>

const std::string nullfile = "/dev/null";

void Daemon(const std::string &cwd = "")
{
    // 1.忽略其他异常信号
    signal(SIGCHLD, SIG_IGN);
    signal(SIGPIPE, SIG_IGN);
    signal(SIGSTOP, SIG_IGN);

    // 2.将自己变成独立的会话
    if (fork() > 0)
        exit(0);
    // setsid 组长不能调用,只有组员可以调用
    setsid();

    // 3.更改当前调用进程的工作目录
    if (!cwd.empty())
        chdir(cwd.c_str());
    
    // 4.标准输入,标准输出,标准错误重定向至 /dev/null
    // 写到 /dev/null 的数据都会被丢弃
    int fd = open(nullfile.c_str(), O_RDWR);
    if (fd > 0)
    {
        dup2(fd, 0);
        dup2(fd, 1);
        dup2(fd, 2);
        close(fd);
    }
}

接下来我们就可以让 TCP 服务器守护进程化,即

void Start()
{
    Deamon();
    lg(Info, "TCP server is running...");
    ThreadPool<Task>::GetInstance()->Start();
    for (;;)
    {
        struct sockaddr_in client;
        socklen_t len = sizeof(client);

        int sockfd = accept(listensock_, (struct sockaddr *)&client, &len);
        if (sockfd < 0)
        {
            lg(Fatal, "listen erron,errno: %d, errstring: %s" , errno, strerror(errno));
            continue;
        }
        
        uint16_t clientport = ntohs(client.sin_port);
        char clientip[32];
        inet_ntop(AF_INET,&(client.sin_addr), clientip, sizeof(clientip));
        lg(Info, "TCP server get a new link, clientip: %s, clientport: %d", clientip, clientport);

        // 线程池版
        Task t(sockfd, clientip, clientport);
        ThreadPool<Task>::GetInstance()->Push(t);
    }
}

运行效果如下

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Excel如何把两列数据合并成一列,4种方法

Excel如何把两列数据合并成一列,4种方法 参考链接:https://baijiahao.baidu.com/s?id=1786337572531105925&wfr=spider&for=pc 在Excel中,有时候需要把两列或者多列数据合并到一列中,下面介绍4种常见方法,并且提示一些使用注意事项,总有一种方法符合你的要求:…

LabVIEW三针自动校准系统

基于LabVIEW的智能三针自动校准系统采用非接触式激光测径仪对标准三针进行精确测量。系统通过LabVIEW软件平台与硬件设备的协同工作&#xff0c;实现了数据自动采集、处理及报告生成&#xff0c;大幅提高了校准精度与效率&#xff0c;并有效降低了人为操作误差。 一、项目背景…

【Java】JDK集合类源码设计相关笔记

文章目录 前言1. Iterable2. RandomAccess2.1 RandomAccess 使用索引进行二分查找 3. Map3.1 HashMap3.2 IdentityHashMap 4. Collections 工具类4.1 Collections.shuffle() 洗牌 前言 目的: 收集JDK集合类的类图。记录一些有意思的设计。将之前写过的文章建立联系。 1. Ite…

macbook外接2k/1080p显示器调试经验

准备工具 电脑 满足电脑和显示器要求的hdmi线或者转接头或者扩展坞 betterdisplay软件 Dell P2419H的最佳显示信息如下 飞利浦 245Es 2K的最佳显示比例如下 首选1152

Stable Diffusion的解读(二)

Stable Diffusion的解读&#xff08;二&#xff09; 文章目录 Stable Diffusion的解读&#xff08;二&#xff09;摘要Abstract一、机器学习部分1. 算法梳理1.1 LDM采样算法1.2 U-Net结构组成 2. Stable Diffusion 官方 GitHub 仓库2.1 安装2.2 主函数2.3 DDIM采样器2.4 Unet 3…

Github 2024-11-16Rust开源项目日报 Top10

根据Github Trendings的统计,今日(2024-11-16统计)共有10个项目上榜。根据开发语言中项目的数量,汇总情况如下: 开发语言项目数量Rust项目10Go项目1Python项目1Lapce:用 Rust 编写的极快且强大的代码编辑器 创建周期:2181 天开发语言:Rust协议类型:Apache License 2.0St…

Redis作为分布式锁,得会避坑

日常开发中&#xff0c;经常会碰到秒杀抢购等业务场景。为了避免并发请求造成的库存超卖等问题&#xff0c;我们一般会用到Redis分布式锁。但是使用Redis分布式锁之前要知道有哪些坑是需要我们避过去的。 1. 非原子操作&#xff08;setnx expire&#xff09; 一说到实现Redis…

Qt、C++实现五子棋人机对战与本地双人对战(高难度AI,极少代码)

介绍 本项目基于 Qt C 实现了一个完整的五子棋游戏&#xff0c;支持 人机对战 和 人人对战 模式&#xff0c;并提供了三种难度选择&#xff08;简单、中等、困难&#xff09;。界面美观&#xff0c;逻辑清晰&#xff0c;是一个综合性很强的 Qt 小项目 标题项目核心功能 棋盘…

Vulnhub靶场案例渗透[12]-Grotesque: 1.0.1

文章目录 一、靶场搭建1. 靶场描述2. 下载靶机环境3. 靶场搭建 二、渗透靶场1. 确定靶机IP2. 探测靶场开放端口及对应服务3. 目录扫描4. 敏感信息获取5. 反弹shell6. 权限提升 一、靶场搭建 1. 靶场描述 get flags difficulty: medium about vm: tested and exported from vi…

git日志查询和导出

背景 查看git的提交记录并下载 操作 1、找到你idea代码的路径&#xff0c;然后 git bash here打开窗口 2、下载所有的日志记录 git log > commit.log3、下载特定日期范围内记录 git log --since"2024-09-01" --until"2024-11-18" 你的分支 > c…

LeetCode Hot100 15.三数之和

题干&#xff1a; 思路&#xff1a; 首先想到的是哈希表&#xff0c;类似于两数之和的想法&#xff0c;共两层循环&#xff0c;将遍历到的第一个元素和第二个元素存入哈希表中&#xff0c;然后按条件找第三个元素&#xff0c;但是这道题有去重的要求&#xff0c;哈希表实现较为…

Vue3、Vite5、Primevue、Oxlint、Husky9 简单快速搭建最新的Web项目模板

Vue3、Vite5、Oxlint、Husky9 简单搭建最新的Web项目模板 特色进入正题创建基础模板配置API自动化导入配置组件自动化导入配置UnoCss接入Primevue接入VueRouter4配置项目全局环境变量 封装Axios接入Pinia状态管理接入Prerttier OXLint ESLint接入 husky lint-staged&#xf…

基于深度学习的文本信息提取方法研究(pytorch python textcnn框架)

&#x1f497;博主介绍&#x1f497;&#xff1a;✌在职Java研发工程师、专注于程序设计、源码分享、技术交流、专注于Java技术领域和毕业设计✌ 温馨提示&#xff1a;文末有 CSDN 平台官方提供的老师 Wechat / QQ 名片 :) Java精品实战案例《700套》 2025最新毕业设计选题推荐…

Linux(命令格式详细+字符集 图片+大白话)

后面也会持续更新&#xff0c;学到新东西会在其中补充。 建议按顺序食用&#xff0c;欢迎批评或者交流&#xff01; 缺什么东西欢迎评论&#xff01;我都会及时修改的&#xff01; 在这里真的很感谢这位老师的教学视频让迷茫的我找到了很好的学习视频 王晓春老师的个人空间…

机器学习中的概率超能力:如何用朴素贝叶斯算法结合标注数据做出精准预测

&#x1f497;&#x1f497;&#x1f497;欢迎来到我的博客&#xff0c;你将找到有关如何使用技术解决问题的文章&#xff0c;也会找到某个技术的学习路线。无论你是何种职业&#xff0c;我都希望我的博客对你有所帮助。最后不要忘记订阅我的博客以获取最新文章&#xff0c;也欢…

01 —— Webpack打包流程及一个例子

静态模块打包工具 静态模块&#xff1a;html、css、js、图片等固定内容的文件 打包&#xff1a;把静态模块内容&#xff0c;压缩、转译等 Webpack打包流程 src中新建一个index.js模块文件&#xff1b;然后将check.js模块内的两个函数导入过来&#xff0c;进行使用下载webpack…

时间类的实现

在现实生活中&#xff0c;我们常常需要计算某一天的前/后xx天是哪一天&#xff0c;算起来十分麻烦&#xff0c;为此我们不妨写一个程序&#xff0c;来减少我们的思考时间。 1.基本实现过程 为了实现时间类&#xff0c;我们需要将代码写在3个文件中&#xff0c;以增强可读性&a…

学习笔记024——Ubuntu 安装 Redis遇到相关问题

目录 1、更新APT存储库缓存&#xff1a; 2、apt安装Redis&#xff1a; 3、如何查看检查 Redis版本&#xff1a; 4、配置文件相关设置&#xff1a; 5、重启服务&#xff0c;配置生效&#xff1a; 6、查看服务状态&#xff1a; 1、更新APT存储库缓存&#xff1a; sudo apt…

【时间之外】IT人求职和创业应知【35】-RTE三进宫

目录 新闻一&#xff1a;京东工业发布11.11战报&#xff0c;多项倍增数据体现工业经济信心提升 新闻二&#xff1a;阿里云100万核算力支撑天猫双11&#xff0c;弹性计算规模刷新纪录 新闻三&#xff1a;声网CEO赵斌&#xff1a;RTE将成为生成式AI时代AI Infra的关键部分 认知…

css3中的多列布局,用于实现文字像报纸一样的布局

作用&#xff1a;专门用于实现类似于报纸类的布局 常用的属性如下&#xff1a; 代码&#xff1a; <!DOCTYPE html> <html lang"zh-CN"> <head><meta charset"UTF-8"><meta name"viewport" content"widthdevic…