Linux网络——套接字编程

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

运行效果如下

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

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

相关文章

‘视’不可挡:OAK相机助力无人机智控飞行!

南京邮电大学通达学院的刘同学用我们的oak-d-lite实现精确打击无人机的避障和目标识别定位功能&#xff0c;取得了比赛冠军。我们盼望着更多的朋友们能够加入到我们OAK的队伍中来&#xff0c;参与到各式各样的比赛中去。我们相信&#xff0c;有了我们相机的助力&#xff0c;大家…

复旦微电子FM33LC046U在keil工程中无法使用j-link下载问题解决

在Keil环境下使用JLINK工具下载程序&#xff0c;发现J-link V7.89a无法识别FM33LC046U&#xff0c;提示如下&#xff1a; 选择Cortex-M0 设置为SW模式&#xff0c;即可识别到芯片 经过如上步骤&#xff0c;就可以使用Jlink下载和仿真程序

java中设计模式的使用(持续更新中)

概述 设计模式的目的&#xff1a;编写软件过程中&#xff0c;程序员面临着来自耦合性&#xff0c;内聚性以及可维护性&#xff0c;可扩展性&#xff0c;重用性&#xff0c;灵活性等多方面的挑战&#xff0c;设计模式是为了让程序&#xff08;软件&#xff09;&#xff0c;具有…

【计算机网络实验】之静态路由配置

【计算机网络实验】之静态路由配置 实验题目实验目的实验任务实验设备实验环境实验步骤路由器配置设置静态路由测试路由器之间的连通性配置主机PC的IP测试 实验题目 静态路由协议的配置 实验目的 熟悉路由器工作原理和机制&#xff1b;巩固静态路由理论&#xff1b;设计简单…

【PS】矢量绘图技巧

1、先使用钢笔工具结合ctrl和alt建将苹果大致扣出来。 任意选择一个颜色进行填充 新建一个图层&#xff0c;使用渐变工具为图层添加渐变颜色 选择剪切蒙版&#xff0c;将图层颜色填入苹果&#xff0c;得最终结果。 内容二、麦当劳 与内容一类似的&#xff0c;使用钢笔工具将M形…

【HCIP]——OSPF综合实验

题目 实验需求 根据上图可得&#xff0c;实验需求为&#xff1a; 1.R5作为ISP&#xff1a;其上只能配置IP地址&#xff1b;R4作为企业边界路由器&#xff0c;出口公网地址需要通过PPP协议获取&#xff0c;并进行CHAP认证。&#xff08;PS&#xff1a;因PPP协议尚未学习&#…

django启动项目报错解决办法

在启动此项目报错&#xff1a; 类似于&#xff1a; django.core.exceptions.ImproperlyConfigured: Requested setting EMOJI_IMG_TAG, but settings are not c启动方式选择django方式启动&#xff0c;以普通python方式启动会报错 2. 这句话提供了对遇到的错误的一个重要线索…

【GeekBand】C++设计模式笔记12_Singleton_单件模式

1. “对象性能” 模式 面向对象很好地解决了 “抽象” 的问题&#xff0c; 但是必不可免地要付出一定的代价。对于通常情况来讲&#xff0c;面向对象的成本大都可以忽略不计。但是某些情况&#xff0c;面向对象所带来的成本必须谨慎处理。典型模式 SingletonFlyweight 2. Si…

计算机网络 (1)互联网的组成

一、互联网的边缘部分 互联网的边缘部分由所有连接在互联网上的主机组成&#xff0c;这些主机又称为端系统&#xff08;end system&#xff09;。端系统可以是各种类型的计算机设备&#xff0c;如个人电脑、智能手机、网络摄像头等&#xff0c;也可以是大型计算机或服务器。端系…

电商行业客户服务的智能化:构建高效客户服务知识库

在电商行业&#xff0c;客户服务是提升用户体验和品牌忠诚度的关键。随着数字化转型的深入&#xff0c;构建一个高效的客户服务知识库变得尤为重要。本文将探讨电商行业如何构建客户服务知识库&#xff0c;并分析其在提升服务质量中的作用。 客户服务知识库的重要性 客户服务…

CentOS 9 无法启动急救方法

方法一&#xff1a;通过单用户安全模式启动 开机按上下方向键&#xff0c;选择需要启动的内核&#xff0c;按e键进入配置模式 修改配置 ro 改 rw 删除 rhgb quiet 末尾增加 init/bin/bash 按 Ctrlx 启动单用户模式 如果想重新启动&#xff0c;重启电脑 执行 exec /sbin/in…

数字后端零基础入门系列 | Innovus零基础LAB学习Day11(Function ECO流程)

###LAB 20 Engineering Change Orders (ECO) 这个章节的学习目标是学习数字IC后端实现innovus中的一种做function eco的flow。对于初学者&#xff0c;如果前面的lab还没掌握好的&#xff0c;可以直接跳过这节内容。有时间的同学&#xff0c;可以熟悉掌握下这个flow。 数字后端…

SAM-Med2D 训练完成后boxes_prompt没有生成mask的问题

之前对着这这篇文章去微调SAM_Med2D(windows环境),发现boxes_prompt空空如也。查找了好长时间问题SAM-Med2D 大模型学习笔记&#xff08;续&#xff09;&#xff1a;训练自己数据集_sam训练自己数据集-CSDN博客 今天在看label2image_test.json文件的时候发现了一些端倪: 官方…

java ssm 同仁堂药品管理系统 在线药品信息管理 医药管理源码jsp

一、项目简介 本项目是一套基于SSM的同仁堂药品管理系统&#xff0c;主要针对计算机相关专业的和需要项目实战练习的Java学习者。 包含&#xff1a;项目源码、数据库脚本、软件工具等。 项目都经过严格调试&#xff0c;确保可以运行&#xff01; 二、技术实现 ​后端技术&…

使用阿里云快速搭建 DataLight 平台

使用阿里云快速搭建 DataLight 平台 本篇文章由用户 “闫哥大数据” 分享&#xff0c;B 站账号&#xff1a;https://space.bilibili.com/357944741?spm_id_from333.999.0.0 注意&#xff1a;因每个人操作顺序可能略有区别&#xff0c;整个部署流程如果出现出入&#xff0c;以…

如何解决VS Code的Live Share会话中Guest无法看到共享的文件夹?

在 VS Code 的 Live Share 会话中&#xff0c;如果 Guest 无法看到共享的文件夹&#xff0c;如图所示&#xff1a; 可能是因为权限设置、浏览器限制或 Live Share 的配置问题。以下是逐步排查和解决问题的方法&#xff1a; 1. 确保正确共享了文件夹 在主机&#xff08;Host&a…

.NET 9 运行时中的新增功能

本文介绍了适用于 .NET 9 的 .NET 运行时中的新功能和性能改进。 文章目录 一、支持修剪的功能开关的属性模型二、UnsafeAccessorAttribute 支持泛型参数三、垃圾回收四、控制流实施技术.NET 安装搜索行为性能改进循环优化感应变量加宽Arm64 上的索引后寻址强度降低循环计数器可…

深入解析TK技术下视频音频不同步的成因与解决方案

随着互联网和数字视频技术的飞速发展&#xff0c;音视频同步问题逐渐成为网络视频播放、直播、编辑等过程中不可忽视的技术难题。尤其是在采用TK&#xff08;Transmission Keying&#xff09;技术进行视频传输时&#xff0c;由于其特殊的时序同步要求&#xff0c;音视频不同步现…

MongoDB:数据迁移

业余人员学习 第一种:通过MongoDB命令 参考链接: MongoDB的备份(mongodump)与恢复(mongorestore)_MongoDB_脚本之家 MongoDB数据库管理:全面掌握mongodump和mongorestore的备份与恢复技巧_8055096的技术博客_51CTO博客 1.1、首先进入操作命令行,都不需要进入MongoDB […

网络安全练习之 ctfshow_web

文章目录 VIP题目限免&#xff08;即&#xff1a;信息泄露题&#xff09;源码泄露前台JS绕过协议头信息泄露robots后台泄露phps源码泄露源码压缩包泄露版本控制泄露源码(git)版本控制泄露源码2(svn)vim临时文件泄露cookie泄露域名txt记录泄露敏感信息公布内部技术文档泄露编辑器…