Linux——基于socket编程实现简单的Tcp通信

前言1:想要实现一个简单的Tcp通信不难,对于初学者而言,难点在于使用了大量未曾接触过的函数调用,所以本篇重点在于详解每部分代码中相关函数的功能。

1. 简单认识一下TCP传输

TCP通信协议是面向字节流的、可靠的、有连接的传输,在实现TCP协议通信时:

①:必须先建立客户端和服务端间的连接这部分由系统函数实现

②:其次因为TCP是面向字节流的,所以它的很多操作和文件操作是一致的

2. 实现思路

 编写一个服务端:TcpServer;

①. 初始化服务端,设置 IP 、 端口号 and 执行方法

②. 运行服务端,接收客户端发来的数据,交由其他线程来处理

 编写一个客户端:TcpClient;

①. 确定目标 ip and 端口号

②. 客户端发送数据到服务端,同时接收服务端的消息

3. 各部分涉及的相关函数详解

3.1 初始化服务端

步骤1:网络也是文件,一种特殊的文件,因此一开始需要打开文件(创建套接字)

相关函数:

int socket(int domain, int type, int protocol);

功能:

        创建 socket 文件描述符 (TCP/UDP, 客户端 + 服务器):

参数:

domain:用于标识是网络通信还是本地通信

        AF_INET:网络通信  ☆☆☆

        AF_UNIX:本地通信

type:用于标识不同的套接字类型

        SOCK_DGRAM:基于UDP协议的通信

        SOCK_STREAM:基于TCP协议的通信

protocol:设置为0时;

        对于 AF_INET 和 SOCK_STREAM,操作系统会选择 TCP 协议

        对于 AF_INET 和 SOCK_DGRAM,操作系统会选择 UDP 协议​​​

返回值:int sockfd = socket(AF_INET, SOCK_STREAM, 0);

        sockfd(套接字网络文件描述符),用于表述对应描述符表的唯一性。

步骤2:套接字创建完毕时,sockfd需要和套接字描述符表进行绑定,就像打开文件时,文件描述符fd会和文件描述符表结构体相关联。

相关函数:

int bind(int sockfd, const struct sockaddr *address, socklen_t address_len);

功能

绑定端口号 (TCP/UDP, 服务器),将当前套接字与ip和端口号绑定

参数

sockfd

        套接字

address

        对于AF_INET通信,strcut sockaddr_in* 结构体指针内部保存的ip

len

        上述结构体对应的大小

注1☆☆☆

        对于服务端而言,将当前服务端所创建的套接字和服务端本地的ip以及指定端口绑定

        在次过程之前,需要将主机字节序转为网络字节序!

        这里能够通过面向对象的方式,来完成一步骤。相关代码如下

class InetAddr
{
public:
    //主机转网络
    InetAddr(u_int16_t port)
        :_port(port)
    {
        memset(&_addr, 0, sizeof(_addr));
        _addr.sin_addr.s_addr = INADDR_ANY;
        _addr.sin_family = AF_INET;
        _addr.sin_port = htons(_port);
    }

    const struct sockaddr* NetAddrptr() { return CONV(_addr); } //获取sockaddr* 地址
    socklen_t NetAddrLen(){ return sizeof(_addr); }

private:
    struct sockaddr_in _addr;
    std::string _ip;
    uint16_t _port;
};

#define CONV(addr) ((struct sockaddr*)&addr)

//类外可以通过相应函数进行构造
//  InetAddr local(_port);
//  int n = bind(sockfd, local.NetAddrptr(), local.NetAddrLen());

注2

        对于服务端,需要显式调用bind,bind的过程中,ip为当前服务器下所有可用ip

        对于客户端,不需显式调用bind,本地服务器在第一次进行网络通信时,操作系统会将本地IP and 随机端口号 分配给 sockfd。

        原因:因为用户不知道哪个端口号现在被占用了,所以需要等待系统去给你分配

        一个端口号只能被一个进程占用!

步骤3 :服务端显式调用bind后,需将当前sockfd设置为监听状态

相关函数:

int listen(int sockfd, int backlog);

sockfd

        当前套接字

backlog

        最大等待连接数

:之所以将当前套接字设置为listen的目的是:可能会有多个客户端向服务端进行通信。

举个简单例子:这里的socket被称为监听套接字,他充当一个在外部揽客的角色。每当有一个客户端向服务端进行通信时,都会被 监听套接字纳入到连接队列。下面会介绍 accpet 函数,该函数会从连接队列中取出一个,和客户端进行通信,这部分由多线程完成。

补充1线程更安全的 字符串 and 网络字节序 转换

相关函数:

int inet_pton(int af, const char *src, void *dst);

功能: 点分字符串 转为 网络字节序  p → n   presentation → network

af:地址族,通常为(AF_INET、AF_INET6)

src: 字符串风格的ip地址

dst:转换后的 网络字节序保存的位置

相关函数:

uint16_t htons(uint16_t hostshort);

功能: 将主机字节序转为网络字节序

hostshort:主机字节序

3.2 运行服务端

步骤1 :多个客户端向服务端发起通信时,此时需从连接队列取出一个客户端(报文)建立通信

相关函数:

int accept(int listen_sockfd, struct sockaddr *addr, socklen_t *addrlen);

功能

        从连接队列取出一个 连接 进行通信

 参数

listen_sockfd:

        从监听套接字中取出,该套接字是唯一的

addr

         用于保存客户端地址信息!!!!

addrlen

        地址结构体大小的指针

:accpet取出的 连接,是客户端的地址信息,因此在accpet前,需要创建一个 struct sockaddr_in 结构体来保存该信息,该信息中包含客服端的ip and 端口号

步骤2:因为接收到的是网络字节序,所以需要将 网络字节序 转为 主机字节序

相关代码如下:

class InetAddr
{
public:
     //网络转主机
    InetAddr(struct sockaddr_in &addr) : _addr(addr)
    {
        _port = ntohs(_addr.sin_port);           // 从网络中拿到的!网络序列
        char ipbuffer[64];
        inet_ntop(AF_INET,&_addr.sin_addr, ipbuffer,sizeof(_addr));// 4字节网络风格的IP -> 点分十进制的字符串风格的IP
        _ip = ipbuffer;
    }
private:
    struct sockaddr_in _addr;
    std::string _ip;
    uint16_t _port;
};


//InetAddr addr(peer);  // peer 为accept接收到的客户端的 struct sockaddr_in 结构体

补充: 线程更安全的 字符串 and 网络字节序 转换

相关函数:

const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);

功能: 网络字节序 转为 字符串    p → n   presentation → network

af:地址族,通常为(AF_INET、AF_INET6)

src: 二进制格式的ip地址

dst:输出的字符串缓冲区

size:缓冲区的大小

相关函数:

uint16_t ntohs(uint16_t Netshort);

功能: 网络字节序 转 主机字节序

Netshort:网络字节序

3.3 客户端初始化

步骤1:客户端需要提供服务端的 ip and 端口号

步骤2:客户端需要通过socket创建套接字,这部分和客户端是一样的

:客户端是否需要bind?

答:需要!但是不是显式的bind,客户端在和服务端通过TCP建立连接 or UDP直接发送报文时,操作系会自动给当前进程绑定一个端口号,将客户端的 ip and 端口号 发给 服务端,因此客户端不需要显式的bind

步骤3:客户端向服务端发送连接请求

相关函数:

int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

• 功能:

        用于发起客户端到服务端的连接请求

• 参数

sockfd:通过socket创建的文件描述符

addr:目标客服端的结构体

addrlen:addr的大小

:需将主机字节序转为网络字节序才能发送

4.图解TCP传输流程

Linux——基于socket编程实现简单的Tcp通信_第1张图片

假设:主机A是客户端、主机B是服务端

        服务端B通过上述函数初始化后,并调用accept接口阻塞等待客户端A发送数据。

        客户端A初始化并通过send/write接口发送数据时,send所发送的数据并不是直接通过网络传输给客服端B,send/witre是将数据拷贝给了发送缓冲区,os会在合适的时候将数据通过网络拷贝给服务端B的接收缓冲区。

        当客户端A向服务端Bconnect建立通信连接时,服务端B会将客户端丢入到连接队列中,并返回一个新的网络文件描述符sockfd来进行业务处理,同样客户端recv/read不是直接从网络中获取数据,而是在服务端B的接收缓冲区中将数据拷贝到上层。

认知

        结论1:对于Tcp协议,通信的双方在传输层都有发送缓冲区和接收缓冲区,因此是全双工通信,二者相互独立

        结论2:以服务端为例,操作系统向缓冲区拷贝数据,上层向缓冲区读数据,这不就是生产消费者模型吗? Tcp协议本质就是四组生产消费者模型

4.1 应用层再谈协议

• 先前结论:协议 → 结构体

        上述图解TCP传输流程是从底层来说的,对于处在应用层的用户,在将数据send/write 进发送缓冲区时,需要将结构体转成一个大的字符串,这一过程叫做序列化,目的是为了方便网络传输。

        对于服务端,接收缓冲区在收到网络中对应客户端的大字符串时,应用层调用recv/read 后,需要反序列化,方便上册的那个处理/阅读

:客户端和服务端都认识该结构体,该结构体就是被称为客户端和服务端约定好的协议。该模式被称为C(client)/S(server)模式

:为什么不直接发送结构体对象?而是将结构体对象序列化转成大字符串?再反序列化读取

:为了兼容性,因为不同语言结构体的对齐方式不同。

• 结论:所谓的协议定制,本质就是在定制双方都能认识的,符合通信和业务需要的结构化数据,所谓的结构化数据,其实就是struct 或者 class

5. 定制自定义协议来实现基于Tcp通信的网络计算器服务

• Tcp协议通信的双方会存在以下现象

1. 收发双方发送和接收次数不对等

2.收发双方是否选择发送和接收由操作系统决定

3.读方一定得读到一个完整的报文,才能进行反序列化的操作,如果在编程过程中没有读到一个完整的报文就处理,会出BUG

:因读取报文不完整导致的问题叫做“粘报“问题

• 因此定制一个协议需要满足

1.结构化的字段,提供 序列化 and 反序列化 的方案

2.解决因为字节流问题,导致读取报文不完整的问题(只用处理读取)

5.1 实现思路

对于服务端,主函数中通过分层结构实现

① 业务层

② 协议层

③ 通信连接层

对于客户端,主函数主要实现

① 初始化

② 请求建立通信连接

③ 发送数据 + 接收结果

5.1.1 业务层

主函数中,通过智能指针创建一个Cal对象

std::unique_ptr cal = std::make_unique();

Cal类如图如下所示

#pragma once
#include "Protocol.hpp"
#include 

class Cal
{
public:
    Response Excute(Request &req)
    {
        Response resp(0, 0);
        switch (req.Oper())
        {
        case '+':
            resp.SetResult(req.X() + req.Y());
            break;
        case '-':
            resp.SetResult(req.X() - req.Y());
            break;
        case '*':
            resp.SetResult(req.X() * req.Y());
            break;
        case '/':
        {
            if (req.Y() == 0)
                resp.SetCode(1); // 除零错误
            else
                resp.SetResult(req.X() / req.Y());
            break;
        }
        case '%':
        {
            if (req.Y() == 0)
                resp.SetCode(2); // 余零错误
            else
                resp.SetResult(req.X() % req.Y());
            break;
        }
        default:
            resp.SetCode(3); // 传参错误
            break;
        }
        return resp;
    }
};

5.1.2 协议层

 主函数中,同样通过创建一个对象Protocol对象来调用接口

std::unique_ptr p = std::make_unique([&cal](Request& req)->Response{
        return cal->Excute(req);
    });

客户端发送的数据经协议处理后,需要将数据交由上层业务处理,因此需通过lamda表达式来实现回调。

规定1:用户端发送给服务端的数据称为请求 , 服务端发送给用户端的数据称为回复

规定2:大字符串以  "len + \r\n + 数据 + \r\n"  的方式通过网络传递,其中len为数据的长度

\r\n为分隔符,通过上述来解决"粘报"问题。

Protocol类中主要需要实现的函数

①. 用户端将请求序列化 or 服务端将回复序列化, 将结构化的数据转为字符串形式,通过json库来实现,这是一个三方库

②. 用户端 加密序列化后的请求 or 客户端 加密序列化后的回复,将数据转成  "len + \r\n + 数据 + \r\n" 的形式

③. 服务端 解密用户端的请求 or 用户端 解密服务端的回复   从网络中收到的 "len + \r\n + 数据 + \r\n" 的形式的大字符串

客户端和服务端间通信流程

. 服务端创建服务器,等待客户端通信,(建立套接字、bind、设置listen、accept)

. 客户端发送通信请求,双方建立通信连接 (建立套接字、connect)

. 客户端将结构化数据转为字符串形式

. 客户端将数据加密为 "len + \r\n + 数据 + \r\n"形式的数据

. 客户端发送数据  (send/write)

. 服务端接收数据  (recv/read)

. 服务器解密收到的数据

. 服务端将有效数据反序列化 

. 服务器调用回调进行业务处理

. 服务端将处理的结果序列化

. 服务端将序列化后的结果加密

. 服务端发送数据

. 客户端接收数据、解密数据、反序列化数据得到结果。

:请求的序列化和反序列化 与 回复的序列化和反序列化是分开的。两者的参数和结果均不同,当然需要分开写。

代码如下

#pragma once
#include 
#include "common.hpp"
#include "Socket.hpp"

using namespace SocketModule;

// client -> server  客户的请求:客户需要将请求序列化到服务端,服务端需要将客户的请求反序列化
class Request
{
public:
    Request()
    {
    }
    Request(int x, int y, char oper)
        : _x(x),
          _y(y),
          _oper(oper)
    {
    }

    std::string Serialize()
    {
        Json::Value root;
        root["x"] = _x;
        root["y"] = _y;
        root["oper"] = _oper;

        Json::FastWriter Writer;
        std::string s = Writer.write(root);
        return s;
    }

    bool Deserialize(std::string& Package)
    {
        Json::Value root;
        Json::Reader reader;
        bool ok = reader.parse(Package, root); // 将Json串读到root中
        if (ok)
        {
            _x = root["x"].asInt();
            _y = root["y"].asInt();
            _oper = root["oper"].asInt();
        }
        return ok;
    }

    int X() { return _x; }
    int Y() { return _y; }
    char Oper() { return _oper; }

    ~Request()
    {
    }

private:
    int _x;
    int _y;
    char _oper;
};

// server -> client  服务端需要将业务处理的结果序列化到客户端,客户端需要反序列业务处理的结果
class Response
{
public:
    Response() {}

    Response(int result, int code) : _result(result), _code(code)
    {
    }

    // 服务端向用户端序列化
    std::string Serialize()
    {
        Json::Value root;
        root["result"] = _result;
        root["code"] = _code;

        Json::FastWriter writer;
        return writer.write(root);
    }
    // 用户端反序列从服务端接收到的数据
    bool Deserialize(std::string &in)
    {
        Json::Value root;
        Json::Reader reader;
        bool ok = reader.parse(in, root);
        if (ok)
        {
            _result = root["result"].asInt();
            _code = root["code"].asInt();
        }
        return ok;
    }

    void SetResult(int result)
    {
        _result = result;
    }

    void SetCode(int code)
    {
        _code = code;
    }

    void ShowResult()
    {
        std::cout << "计算结果是:" << _result << " [" << _code << "]" << std::endl;
    }

    ~Response() {}

private:
    int _result;
    int _code;
};

const std::string seq = "\r\n";
using func_t = std::function;

class Protocol
{
public:
    Protocol(func_t func)
        : _func(func)
    {
    }

    Protocol()
    {
    }

    // 编码 Encode      len/r/n json码 /r/n  约定通信的双方以这种方式(协议)进行通信,json码为有效数据
    std::string Encode(std::string &jsonstr)
    {
        std::string len = std::to_string(jsonstr.size());       //
        return (len + seq + jsonstr + seq);
    }

    // 解码 Decode 通信的双方以该协议进行通信
    bool Decode(std::string &buffer, std::string *Json_Package)
    {
        // 通信的双方通过 数据长度 + /r/n + json串 + /r/n 进行加密
        // 1.先找第一个 /r/n,没找到说明数据不完整

        ssize_t pos = buffer.find(seq);
        if (pos == std::string::npos)
        {   
            return false;
        }
            // 到这说明该报文有数据长度
        // 2.获得数据长度
        std::string package_len_str = buffer.substr(0, pos);
        int len = std::stoi(package_len_str);


        // 3.根据加密协议 以及数据长度,得到有效报文的总长度
        int size = package_len_str.size() + 2 * seq.size() + len;


        // 4.如果当前服务端收到的数据长度小于 size  说明收到的消息不完整
        if (buffer.size() < size)
        {   
            return false;
        }

        // 5.到这说明buffer内至少包含一条完整的加密后数据,对加密数据进行解密
        *Json_Package = buffer.substr(pos + 2, len);

        // 6.将解密后的数据从buffer中删除
        buffer.erase(0, size);

        return true;
    }
    void GetRequest(std::shared_ptr &sock, InetAddr &client)
    {
        std::string buffer_queue;
        while (true)
        {
            int n = sock->Recv(&buffer_queue);
            if (n > 0)
            {
                // 读取不完整,避免粘包问题
                // 服务端从用户端读取用户请求  → 经客户端序列化的数据且加密的数据
                // 1. 服务端解密
                std::string Json_Package;
                while (Decode(buffer_queue, &Json_Package))
                {

                    // 2.服务端对客户请求的反序列化
                    Request req;
                    bool ret = req.Deserialize(Json_Package);
                    if (!ret)
                        continue;

                    // 3. 解密反序列化完毕后 调用上层业务
                    

                    Response resp = _func(req); // 要将业务处理的结果返回给客户端,所以需要返回一个Response对象

                    // 将得到的结果序列化传递给 用户端
                    // 4.先序列化  服务端 → 用户端的序列化
                    std::string send_str = resp.Serialize();

                    // 5.序加密
                    std::string package = Encode(send_str);
                    // 6.发送
                    sock->Send(package);
                }
            }
            else if (n == 0)
            {
                LOG(LogLevel::INFO) << "用户退出了";
                break;
            }
            else
            {
                LOG(LogLevel::WARNING) << "client" << client.StringAddr() << ": recv error";
                break;
            }
        }
    }

    bool GetResponse(std::shared_ptr client, std::string &resp_buffer, Response *resp)
    {
        while (true)
        {
            int n = client->Recv(&resp_buffer);
            if (n > 0)
            {
                std::string json_package;
                while (Decode(resp_buffer, &json_package))
                {
                    resp->Deserialize(json_package);
                }
                return true;
            }
            else if (n == 0)
            {
                return false;
            }
            else
            {
                return false;
            }
        }
    }

    std::string BuildRequestString(int x, int y, char oper)
    {
        Request req(x, y, oper);
        // 将客户端的请求序列化
        std::string json_str = req.Serialize();
        // 将序列化的json串加密
        return Encode(json_str);
    }

    ~Protocol()
    {
    }

private:
    // 因为我们用的是多进程
    // Request _req;
    // Response _resp;
    func_t _func;
};

5.1.3 通信连接层

主函数中,通过智能指针创建一个TcpServer对象,当服务端收到客户端的通信请求时,创建子进程的子进程来执行回调方法实现通信双方数据的交互

std::unique_ptr tsvr = std::make_unique(std::stoi(argv[1]),[&p](std::shared_ptr &sock, InetAddr &client){
        p->GetRequest(sock, client);
    });

tsvr->Start();

通信连接层通过模板方法模式进行初始化

模板方法模式:将基类中的虚函数方法在派生类中重写,同时基类中有一套固定方法,创建派生类对象时,调用该方法来初始化派生类对象的过程

补充:套接字的创建、绑定、监听、数据的发送、接收和连接,都和网络文件描述符相关,因此我们可以创建一个专门用来调用这些接口的自定义类,该类由书上的模板方法模式实现。具体代码如下所示:
 

#pragma once
#include "common.hpp"
#include "InetAddr.hpp"
#include 

namespace SocketModule
{
    using namespace LogModule;

    class Socket
    {
    public:
        virtual void SocketOrDie() = 0;
        virtual void BindOrDie(uint16_t port) = 0;
        virtual void ListenOrDie(int backlog) = 0;
        virtual std::shared_ptr Accept(InetAddr* Client) = 0;
        virtual bool Connect(std::string& ip, uint16_t port) = 0;
        virtual void Close() = 0;
        virtual int Recv(std::string* out) = 0;
        virtual int Send(const std::string& message) = 0;

    public:
        void BuildTcpSocketMethod(int port, int backlog = 16)
        {
            SocketOrDie();
            BindOrDie(port);
            ListenOrDie(backlog);
        }
        void BulidClientSocketMethod()
        {
            SocketOrDie();
        }
    };

    const int DefaultNum = -1;

    class TcpSocket : public Socket
    {
    public:
        TcpSocket()
            : _sockfd(DefaultNum)
        {
        }
        TcpSocket(int fd)
            : _sockfd(fd)
        {
        }

        void SocketOrDie() override
        {
            _sockfd = ::socket(AF_INET, SOCK_STREAM, 0);
            if (_sockfd < 0)
            {
                LOG(LogLevel::FATAL) << "socket error";
                exit(SOCKET_ERROR);
            }
            LOG(LogLevel::INFO) << "socket sunccess";
        }
        void BindOrDie(uint16_t port) override
        {
            InetAddr localAddr(port);
            int n = ::bind(_sockfd, localAddr.NetAddrptr(), localAddr.NetAddrLen());
            if (n < 0)
            {
                LOG(LogLevel::FATAL) << "bind error";
                exit(BIND_ERROR);
            }
            LOG(LogLevel::INFO) << "bind sunccess";
        }
        void ListenOrDie(int backlog) override
        {
            int n = ::listen(_sockfd, backlog);
            if (n < 0)
            {
                LOG(LogLevel::FATAL) << "listen error";
                exit(LISTEN_ERROR);
            }
            LOG(LogLevel::INFO) << "listen sunccess";
        }
        std::shared_ptr Accept(InetAddr* Client) override
        {
            struct sockaddr_in peer;
            socklen_t len = sizeof(len);
            int fd = ::accept(_sockfd, CONV(peer), &len);
            if (fd < 0)
            {
                LOG(LogLevel::WARNING) << "accept warning ...";
                return nullptr; // TODO
            }
            Client->SetAddr(peer);// 解耦,让另一个对象来管理客户端的ip and 端口号信息
            return std::make_shared(fd);
        }

        bool Connect(std::string& ip, uint16_t port) override
        {
            InetAddr client(ip,port);//初始化客户端
            int n = ::connect(_sockfd,client.NetAddrptr(),client.NetAddrLen());
            if(n < 0)
            {
                LOG(LogLevel::FATAL) << "connnect error";
                return false;
            }
            return true;
        }
        
        
        int Recv(std::string* out) override
        {
            char buffer[1024];
            ssize_t n = ::recv(_sockfd, buffer, sizeof(buffer) - 1, 0);
            if(n > 0)
            {
                buffer[n] = 0;
                *out += buffer;
            }
            return n;
        }

        virtual int Send(const std::string& message) override
        {
            return send(_sockfd, message.c_str(), message.size(), 0);
        }

        void Close() override
        {
            if(_sockfd >= 0)
                ::close(_sockfd);
        }
        ~TcpSocket()
        {
        }

    private:
        int _sockfd;
    };
}

主函数在实例化时,默认的构造函数如图所示

TcpServer(u_int16_t port, ioservice_t func)
        :_ListenSockfd(std::make_unique())
        ,_port(port)
        ,_isrunning(false)
        ,_service(func)
    {
        _ListenSockfd->BuildTcpSocketMethod(port);
    }

在创建过程中,会自动将服务端初始化

5.1.4 客户端

#include "Protocol.hpp"
#include "Socket.hpp"
#include "InetAddr.hpp"
#include "common.hpp"

void GetDataFromStdin(int* x,int* y,char* opre)
{
    std::cout << "please enter x:";
    std::cin >> *x;
    std::cout << "please enter y:";
    std::cin >> *y;
    std::cout << "please enter oper:";
    std::cin >> *opre;
}



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

    std::string ip = argv[1];
    uint16_t port = std::stoi(argv[2]);
    std::shared_ptr client = std::make_shared();

    client->BulidClientSocketMethod();

    if(!client->Connect(ip,port))
    {
        exit(2);
    }

    std::unique_ptr protocol = std::make_unique();
    std::string resp_buffer;
    while(true)
    {
        int x, y;
        char oper;
        GetDataFromStdin(&x,&y,&oper);

        std::string req_str = protocol->BuildRequestString(x,y,oper);

        //发送

        client->Send(req_str);

        //获取应答
        Response resp;
        bool res = protocol->GetResponse(client,resp_buffer,&resp);

        resp.ShowResult();
    }
    client->Close();
    return 0;
}

6. 守护进程

在认识守护进程之前,先认识几个其他的概念

• 进程组(PGID)

:进程是以进程组的方式完成对应任务,相当于公司内部的一个团队,负责一个任务。

:具有多个进程的进程组而言,进程组的id与父进程id一致。

:单个进程的进程组id等于当前进程id

 进程组组长的作用:进程组组长可以创建一个进程或者创建该组中的进程

 进程组的生命周期:从进程组船舰开始到其中最后一个进程结束为止,进程组的存在与否与组长是否存在无关。

• 会话: 

:同一个进程组里的会话id是相同的,值为当前进程组的父进程

:当系统成功登录时,至少会先创建一个会话,该会话包含一个终端文件,终端文件 是内核与用户交互的接口

:前台进程组能够收到键盘产生的信号,而后台进程组不会

:一个会话可以包含多个进程组,一个进程组只能属于一个会话

举例:登录windows,就是建立会话的过程, 然后加载图形化界面(桌面)(即外壳)对于linux而言,他的图形化界面是命令行,所有的作业/任务 都在同一个会话内部进行

• 登录是建立会话的过程

• 关闭终端,就是销毁会话的过程 → 会影响服务器的运行,但是正常服务器中,用户的登录和注销不会影响服务器

:怎么做服务器才能避免受用户登录和注销的影响?

:将网络服务器丢到一个新的会话,丢入到新会话的进程叫做 守护进程/精灵进程

守护进程:后台进程依旧属于当前会话中,守护进程也是后台进程,但是他具有独立会话,  如果服务器是守护进程的话,一般以d结尾

变成守护进程的函数接口:当进程调用 pit_t setsid(); 就会变成守护进程  

:前提不能是进程组的组长,可以创建子进程,关闭父进程来避免这种情况,所以守护进程本质也是一个孤儿进程

补充:/dev/null 路径 

凡是写入到该文件的内容都会被丢弃,凡是从该文件读取的内容都是空。

如果不想看到任何输入、输出、错误信息,可以重定向到该路径下。

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

            int fd = open(dev.c_str(), O_RDWR)

            dup2(fd, 0);
            dup2(fd, 1);
            dup2(fd, 2);

            close(fd);

6.1 将进程设置为守护进程的两种方式

1. 自定义函数方式

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

// 将服务进行守护进程化的服务
void Daemon(int nochdir, int noclose)
{
    // 1. 忽略IO,子进程退出等相关的信号
    signal(SIGPIPE, SIG_IGN);
    signal(SIGCHLD, SIG_IGN); // SIG_DFL

    // 2. 父进程直接结束
    if (fork() > 0)
        exit(0);

    // 3. 只能是子进程,孤儿了,父进程就是1
    setsid(); // 成为一个独立的会话

    if(nochdir == 0) // 更改进程的工作路径
        chdir("/");

    // 4. 依旧可能显示器,键盘,stdin,stdout,stderr关联的.
    //  守护进程,不从键盘输入,也不需要向显示器打印
    //  打开/dev/null, 重定向标准输入,标准输出,标准错误到/dev/null
    if (noclose == 0)
    {
        int fd = ::open(dev.c_str(), O_RDWR);
        if (fd < 0)
        {
            //OPEN_ERR
            exit(1);
        }
        else
        {
            dup2(fd, 0);
            dup2(fd, 1);
            dup2(fd, 2);
            close(fd);
        }
    }
}

 

2.调用系统接口

int daemon(int nochdir, int noclose);

• nochdir

0:切换工作目录到 /

非 0:保留当前工作目录

• noclose:

0:关闭并重定向 stdin/stdout/stderr 到 /dev/null

非 0:保留原来的文件描述符

你可能感兴趣的:(Linux,tcp/ip,网络,服务器)