Linux套接字编程详解

预备知识

IP地址和MAC地址

MAC地址用来在局域网中标识唯一主机
Ip地址用于在广域网中标识唯一主机
(1)IP地址:

IP协议有两个版本 , IPv4 和IPv6. 我们凡是提到 IP 协议 , 没有特殊说明的 , 默认都是指 IPv4 IP 地址是在 IP
协议中 , 用来标识网络中不同主机的地址 ; 对于 IPv4 来说 , IP 地址是一个4字节, 32位的整数; 我们通常也使用
“点分十进制” 的字符串表示IP地址, 例如 192.168.0.1 ; 用点分割的每一个数字表示一个字节, 范围是 0 - 255;

(2)MAC地址:

MAC地址是物理网卡硬件地址 :用于识别相邻的两个物理硬件设备,它的大小为:6字节 ①长度为48位, 及6个字节 .
一般用16进制数字加上冒号的形式来表示(例如: 08:00:27:03:fb:19)
②在出厂时就会设定,不能修改,MAC地址通常是唯一的,它的大小是6字节,用于识别相邻设备,在链路层完成相邻设备之间的数据传输。
(虚拟机中的mac地址不是真实的mac地址, 可能会冲突; 也有些网卡支持用户配置mac地址). ③MAC地址与网络无关
④一台计算机可以有多个MAC地址:一台计算机可以绑定多个网卡,进而可以拥有多个MAC地址。
举例:IP数据报的收发方进行跨网投递时,发送方需利用ARP协议获取
发送方本网段路由器对应端口的MAC地址,因为当需要跨网络进行传递的时候,也就是意味着需要找到该数据包的下一跳的MAC地址,所以认为从发送方出来,首先先到到达本网段的路由器,所以获取本网段的路由器的MAC地址

套接字结构

套接字由IP地址和端口号组成,其中端口号标识唯一进程。
主机间在通信的本质是:在各自的主机上的两个进程在互相交互数据!
IP地址可以完成主机和主机的通信,而主机上各自的通信进程,才是发送和接受数据的一方

IP :确保主机的唯一性
端口号(port):确保该主机上某一个进程的唯一性(则一个进程只能占用一个端口号)
IP:PORT = 标识互联网中唯一的一个进程!——>这两个合起来叫 socket(套接字)(翻译是插座)
网络通信的本质:就是进程间通信! ! !
端口号(port)是传输层协议的内容:
端口号是一个 2字节16位的整数 ;类型是uint16_t,不过传uint32_t也可以,最终会截断成uint16_t。 因为一般1-1023端口属于系统保留端口,这些端口已经分配给一些应用了,所以我们只能使用1024及以上的端口。
端口号是进程的门,如果一个进程有多个门,那我可以接受多路信息。
如果一个端口可以去多个进程,那么就会出问题,端口就变成十字路口了。
注1: 一个端口号只能被一个进程占用(一个进程可以有多个端口号,但一个端口号不可以对应多个进程,只要保证从端口号到进程的数据链路是唯一的 )
注2: Socket客户端的端口是不固定的, Socket服务端的端口是固定的。
解释:客户端的端口我们推荐是不主动绑定策略,这样可以尽可能的避免端口冲突,让系统选择合适端口绑定,因此不固定;
服务端的端口必须是固定的,因为总是客户端先请求服务端,因此必须提前获知服务端地址端口信息,但是一旦服务器端端口改变,会造成之前的客户端的信息失效找不到服务端了。
思考一下 服务端为什么是固定的? 客服端为什么是动态的?
因为高铁站修好了就不动了,而你可以一直搬家。
理解 “端口号” 和 “进程ID”(端口号的意义)
端口号和进程ID的区别,端口号是进程在网络的户口,进程ID是在操作系统的户口。这样更加方便分层管理。

网络字节序

网络字节序在网络中是大端。可能大家已经忘了大小端,下面我们在介绍一下大小端。
在这里插入图片描述
大端存储在网络中是规定的。
网络和主机字节序的转换函数:
为使网络程序具有可移植性 , 使同样的 C 代码在大端和小端计算机上编译后都能正常运行 , 可以调用以下库函数做网络字节序和主机字节序的转换
在这里插入图片描述

uint32_t htonl (uint32_ t hostlong);
 ——htonl(host to net 主机转网络)

下面是这四个函数的详细讲解:

htonl (host to network long)

功能:将32位无符号整数从主机字节序转换为网络字节序(大端字节序)。

参数:一个32位无符号整数(通常表示IPv4地址或端口号)。

返回值:转换后的32位无符号整数(网络字节序)。

htons (host to network short)

功能:将16位无符号整数从主机字节序转换为网络字节序(大端字节序)。

参数:一个16位无符号整数(通常表示端口号)。

返回值:转换后的16位无符号整数(网络字节序)。

ntohl (network to host long)

功能:将32位无符号整数从网络字节序(大端字节序)转换为主机字节序。

参数:一个32位无符号整数(网络字节序)。

返回值:转换后的32位无符号整数(主机字节序)。

ntohs (network to host short)

功能:将16位无符号整数从网络字节序(大端字节序)转换为主机字节序。

参数:一个16位无符号整数(网络字节序)。

返回值:转换后的16位无符号整数(主机字节序)。

①这些函数名很好记,h表示host,n表示network,l表示32位长整数,s表示16位短整数。例如htonl表示以32位的长整数为单位从主机字节序转换为网络字节序,例如将IP地址转换后准备发送。
②如果主机是小端字节序,这些函数将参数做相应的大小端转换然后返回;
③如果主机是大端字节序,这些函数不做转换,将参数原封不动地返回。

所以主机必须可具备大小端转换,并且保证发到网络中的数据是大端数据。

UDP套接字编程

接下来我们介绍套接字编程,首先介绍流程:
在这里插入图片描述
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的数据类型:sockaddr结构(套接字的地址结构类型定义)
在这里插入图片描述
前16位是标志,是数据结构的名字。通用数据类型sockaddr,sockaddr_in和sockaddr_un是sockaddr是他们的统一形式,为了方便传参。

struct sockaddr_ in——网络套接字,用于网络通信;
struct sockaddr_un——域间套接字,用于UNIX本地通信。

下面我们详细介绍struct sockaddr_in

struct sockaddr_in {  
    short            sin_family;   // 地址族,通常为 AF_INET  
    unsigned short   sin_port;     // 端口号,网络字节序  
    struct in_addr   sin_addr;     // IPv4 地址  
    char             sin_zero[8];  // 填充至 struct sockaddr 的大小,通常不用  
};

字符串风格的IP地址转为4字节地址 inet_addr
4字节转字符串 inet_ntoa

in_addr_t inet_addr(const char *cp); 

网络服务 recvfromsendto

 ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen);

从特定套接字 sockfd中读取数据到缓冲区buf中,buf大小为len,flags设为0——阻塞式读取

src_addr:(输出型参数)当服务器读取客户端发送的消息时——哪个客户端给你发的消息,就把这个客户端套接字信息存入src_addr中。(src_addr的类型是套接字类型指针struct
sockaddr*,传入的网络套接字类型struct sockaddr_in需要强转成此类型指针 struct sockaddr。)

addrlen:(输入输出型参数)客户端这个缓冲区大小。(socklen_t就是unsigned int)

返回值:返回读到的字节数,错误就返回-1错误码被设置

该接口为阻塞方式接口。接收端收到消息后,就已经知道发送方的套接字,并不需要再次接收。

socket编程三部曲: 1创 2绑 3发

部分细节解释+代码(udp套接字)
易错:1. port_ 端口号是一个 2字节16位的整数,主机转网络要用htons,不能用htonl.16位是短整型
server.sin_port=htons(server_port);

htonl 是转换四字节的,如果你传入一个两字节的数据,它就会自动进行补位,补位前面部分都是零,那这时候经过htonl置换之后,前16位就变成零了,相当于你的程序跑去绑定零端口去了,就会绑定失败。

(1)INADDR_ANY

 #define INADDR_ANY    ((in_addr_t) 0x00000000)
local.sin_addr.s_addr = ip_.empty() ? htonl(INADDR_ANY) : inet_addr(ip_.c_str());

①INADDR_ANY (这个宏的值就是0): 程序员不关心会bind到哪一个ip, 任意地址bind,强烈推荐的做法,所有服务器一般的做法(解释:一般服务器只有一个IP,会自动bind这个IP;如果服务器有多个IP,会自动bind这个服务器的所有的IP——因为如果有两个IP:IP1和IP2,只bind一个IP1,那么只有传给IP1的报文会交给程序,IP2就不会提交报文)

云服务器有一些特殊情况:禁止你bind云服务器上的任何确定IP, 所以这里只能使用INADDR_ANY,如果你是虚拟机就可以bind自己虚拟机的IP,用ifconfig查看IP。

注意:这里inet_addr(ip_.c_str()) 当ip_是"0"时 等价于INADDR_ANY,INADDR_ANY 这个宏的值就是0,0是字符串风格还是网络风格无所谓,并且inet_addr 还会自动给我们进行 h—>n 主机字节序转网络字节序,即 inet_addr(0)=inet_addr(INADDR_ANY)=htonl(INADDR_ANY) 作用是一样的
UDPsocke的创建
1.创 创建UDPsocket文件描述符:

sockfd_=socket(AF_INET,SOCK_DGRAM,0);
if(sockfd_<0)
{
    log(Fatal,"socket creat error, sockfd : %d",sockfd_);
    exit(SOCKET_ERR);
}

在这里插入图片描述
2 绑 绑定之前需要设定好需要被绑定的信息:
在这里插入图片描述
接下来我们详细介绍一下sockaddr_in结构体的内部:
在这里插入图片描述

服务端代码

这就是他的结构体类型,我开始依次绑定三个信息地址族,端口号,IPV4地址信息。

local.sin_family=AF_INET;//表示我使用IPV4协议族
local.sin_port=htons(port_);//字节序的转换,不管你是什么字节序,在发送时都必须转换为网络字节序
local.sin_addr.s_addr=inet_addr(ip_.c_str());
//字符串风格ip转转ip 

inet_addr有两个功能一个是字符串转ip 。
一个是主机序转网络序。
3发 发送消息,其实就是像文件中写入。

recvfrom
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen);

在这里插入图片描述

inet_ntoa(client.sin_addr)
32位IPV4转换为点分十进制IP
ntohs(client.sin_port)
网络序转

刚刚我们写完了服务器,现在我们来描述一下客服端。
客服端可以主动给服务器发消息,所以我们需要知道,客服端到底给谁发以及发什么。
所以我可以用我们之前学的命令行参数,直接给main函数传参。

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

我们可以直接将通信的ip 以及端口号 传进去。组成基本套接字。

客服端代码

客服端 也是1创 2绑 3发
接下来我们继续创建客服端:

1 创 创建端口

   int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    if (sockfd < 0)
    {
        cout << "socker error" << endl;
        return 1;
    }

2 绑 绑定之前先写入IP信息

struct sockaddr_in client;

client.sin_family=AF_INET; 
client.sin_port=htons(serverport);//端口号 转换 
client.sin_addr.s_addr=inet_addr(serverip.c_str()); // 字符串风格IP转网络字节序整数
socklen_t len =sizeof(client);

    // client 要bind吗?要!只不过不需要用户显示的bind!一般有OS自由随机选择!
    // 一个端口号只能被一个进程bind,对server是如此,对于client,也是如此!
    // 其实client的port是多少,其实不重要,只要能保证主机上的唯一性就可以!
    // 系统什么时候给我bind呢?首次发送数据的时候

注意: 服务器需要固定的端口号和ip地址 客服端不需要!旅客可以变,旅馆不能变。

小贴士: 可以通过netstat -naup 查看端口号
在这里插入图片描述
2. 云服务禁止绑定公网IP
3. 0-1023系统内定了,不能使用。都被固定应用层用了。

  1. 注意 客服端的端口号并不需要固定,服务器的端口号是固定的,由你选择的协议而定

我们知道,服务器 IP和端口号固定。但是用户端 ip和端口都不需要固定 。我们测试这个服务器的端口号。
在这里插入图片描述
我们通过测试发现,系统每次绑定的并不是同一个。
接下来我们附上udp代码:

#pragma once

#include <iostream>
#include <string>
#include <strings.h>
#include <cstring>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <functional>
#include "Log.hpp"
using namespace std;
// using func_t = std::function<std::string(const std::string&)>;
typedef std::function<std::string(const std::string&)> func_t;

extern Log lg;

enum{
    SOCKET_ERR=1,
    BIND_ERR
};

uint16_t defaultport = 8080;
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)
    :sockfd_(0), port_(port), ip_(ip),isrunning_(false)
    {}
    void Init()
    {
        // 1. 创建udp socket
        sockfd_ = socket(AF_INET, SOCK_DGRAM, 0); // PF_INET
        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
        struct sockaddr_in local;
        bzero(&local, sizeof(local));
        local.sin_family = AF_INET;
        local.sin_port = htons(port_); //需要保证我的端口号是网络字节序列,因为该端口号是要给对方发送的
        local.sin_addr.s_addr = inet_addr(ip_.c_str()); //1. string -> uint32_t 2. uint32_t必须是网络序列的 // ??
        // local.sin_addr.s_addr = htonl(INADDR_ANY);

        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 success, errno: %d, err string: %s", errno, strerror(errno));
    }
    void Run() // 对代码进行分层
    {
        isrunning_ = true;
        char inbuffer[size];
        while(isrunning_)
        {
            struct sockaddr_in client;
            socklen_t len = sizeof(client);
            ssize_t n = recvfrom(sockfd_, inbuffer, sizeof(inbuffer) - 1, 0,
             (struct sockaddr*)&client, &len);
          
          sockaddr_in clientTmp=(sockaddr_in)client;
         string IP(inet_ntoa(clientTmp.sin_addr));
        uint16_t p=ntohs(client.sin_port);
            if(n < 0)
            {
                lg(Warning, "recvfrom error, errno: %d, err string: %s", errno, strerror(errno));
                continue;
            }
            inbuffer[n] = 0;
            
                string buffer=inbuffer;
            std::string info ;
            info="client@:"+buffer;
            cout<<"test:"<<p<<endl;
            cout<<"test:"<<info<<" client ip:"<<IP<<" client port:"<<(uint16_t)p<<endl;
            sendto(sockfd_, info .c_str(), info .size(), 0, (const sockaddr*)&client, len);

        }
    }
    ~UdpServer()
    {
        if(sockfd_>0) 
        close(sockfd_);
    }
private:
    int sockfd_;     // 网路文件描述符
    std::string ip_; // 任意地址bind 0
    uint16_t port_;  // 表明服务器进程的端口号
    bool isrunning_;
};

#pragma once
#include <iostream>
#include <pthread.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <functional>
#include <vector>
#include <string>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <functional>
#include <time.h>
#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>
#include <mutex> 
#include<netinet/in.h>
#include<string.h>
#include"Log.hpp"
#include<semaphore.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
using namespace std;

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

int main(int argc, char *argv[])//启动客服端必须告知你要访问的ip 端口等信息
{
    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_DGRAM,0);
    if(sockfd<0)
    {
        cout<<"socker error"<<endl;
        return 1;
    }

    struct sockaddr_in client;

    client.sin_family=AF_INET; 
    client.sin_port=htons(serverport);//端口号 转换 
    client.sin_addr.s_addr=inet_addr(serverip.c_str()); // 字符串风格IP转网络字节序整数
    socklen_t len =sizeof(client);

    // client 要bind吗?要!只不过不需要用户显示的bind!一般有OS自由随机选择!
    // 一个端口号只能被一个进程bind,对server是如此,对于client,也是如此!
    // 其实client的port是多少,其实不重要,只要能保证主机上的唯一性就可以!
    // 系统什么时候给我bind呢?首次发送数据的时候

    string message;
    char buffer[1024];
    while (true)
    {
        cout << "Please Enter@ ";
        getline(cin, message);

         //std::cout << message << std::endl;
        // 1. 数据 2. 给谁发
        sendto(sockfd, message.c_str(), message.size(), 0, (struct sockaddr *)&client, len);
        
        struct sockaddr_in temp;
        socklen_t lent = sizeof(temp);

        ssize_t s = recvfrom(sockfd, buffer, 1023, 0, (struct sockaddr*)&temp, &lent);
        if(s > 0)
        {
            buffer[s] = '\0';    
            cout << buffer << endl;
        }
    }

    close(sockfd);
    return 0;
}

TCP 套接字

再次介绍地址族和套接字族:
地址族(Address Family)

地址族是指网络中主机的地址类型。在网络编程中,地址族决定了套接字所使用的网络通信协议和地址格式。

AF_INET:这是一个常用的地址族,表示使用IPv4协议。IPv4是互联网上使用最广泛的协议之一,它使用32位的IP地址。当你创建一个使用IPv4地址的套接字时,你会使用AF_INET作为地址族参数。
AF_INET6:这个地址族用于IPv6协议。IPv6是IPv4的下一代协议,它使用128位的IP地址,提供了更多的地址空间和其他一些改进。

套接字类型(Socket Type)

套接字类型决定了套接字的工作方式和特性。不同的套接字类型适用于不同的应用场景。

SOCK_STREAM:这是一个面向连接的套接字类型,通常用于TCP协议。它提供了可靠、有序的、基于字节流的通信。

SOCK_DGRAM:这是一个无连接的套接字类型,通常用于UDP协议。它提供了不可靠的、基于数据报的通信。

介绍完之后,我们依然在初始化的时候对,TCP服务器开始三步走

//1 创
sockfd_=socket(AF_INET, SOCK_STREAM,0);
 if(sockfd_<0)
 {
log(Fatal,"socket is worring");
exit(-1);
 }
 log(Info,"sockfd is %d",sockfd_);
//2 写
 struct sockaddr_in server;
 memset(&server,0,sizeof(server));
 server.sin_family=AF_INET;
 server.sin_port=htons(port_);
 server.sin_addr.s_addr=inet_addr(ip_.c_str());

//3 绑
if(bind(sockfd_, (const struct sockaddr *)&server, sizeof(server))<0)
{
    log(Fatal,"bind errno");
    exit(-1);
}
if(listen(sockfd_,backlog)<0)
{
    log(Fatal,"listen errno");
    exit(-1);
}

TCP多了一步 听。TCP是面向链接的,建立链接了才能发。

void Run()
{
    log(Info,"Tcp is running");
while(1)
{
    //创建新链接 sockfd
struct sockaddr_in client;
socklen_t len =sizeof(client);
 int sockfd=accept(sockfd_,(sockaddr*)&client,&len);
// 根据新链接通信
 
if(sockfd<0)
{
    log(Warning,"accecpt is waitting");
    continue;
} 
char* ipstr=new char[32];
   uint16_t clientport =ntohs(client.sin_port);
   inet_ntop(AF_INET,&(client.sin_addr),ipstr,32);
   
log(Info,"get a new link..., sockfd:%d, client ip: %s\n ",sockfd,ipstr);

}
}

inet_ntop(AF_INET,&(client.sin_addr),ipstr,32);

在本接口中输入型参数。用来获取转换后的ip。该函数没有线程不安全的问题。

查看TCP网络服务器情况和端口使用情况 netstat -nltp
在这里插入图片描述
注意:TCP是流式套接字,我们用wirte写入,read读取。
在这里插入图片描述
TCP服务端的创建
1创 2写 3绑

class Tcpserver
{
public:
Tcpserver(const uint16_t port=DEAFLITPORT,const string ip="0.0.0.0")
:port_(port),sockfd_(0),ip_(ip)
{

}

void Init()//Tcp初始化
{
//1 创
sockfd_=socket(AF_INET, SOCK_STREAM,0);
 if(sockfd_<0)
 {
log(Fatal,"socket is worring");
exit(-1);
 }
 log(Info,"sockfd is %d",sockfd_);
//2 写
 struct sockaddr_in server;
 memset(&server,0,sizeof(server));
 server.sin_family=AF_INET;
 server.sin_port=htons(port_);
 server.sin_addr.s_addr=inet_addr(ip_.c_str());

//3 绑
if(bind(sockfd_, (const struct sockaddr *)&server, sizeof(server))<0)
{
    log(Fatal,"bind errno");
    exit(-1);
}
if(listen(sockfd_,backlog)<0)
{
    log(Fatal,"listen errno");
    exit(-1);
}

}


void Run()
{
    int sockfd;
    log(Info,"Tcp is running");
while(1)
{
    //创建新链接 sockfd
struct sockaddr_in client;
socklen_t len =sizeof(client);
 sockfd=accept(sockfd_,(sockaddr*)&client,&len);
// 根据新链接通信
 
if(sockfd<0)
{
    log(Warning,"accecpt is waitting");
    continue;
} 
char* ipstr=new char[32];
   uint16_t clientport =ntohs(client.sin_port);
   inet_ntop(AF_INET,&(client.sin_addr),ipstr,32);
   
log(Info,"get a new link..., sockfd:%d, client ip: %s\n ",sockfd,ipstr);
break;
}
string infomassage ;

while(1)
{
                   infomassage.clear();
             char *str=new char[1024];
          ssize_t s = read(sockfd, str, strlen(str));

        
          infomassage=str;

          if(s>0)
          {
            string tmp("revice:");
              
                  infomassage+=tmp;
             size_t num=write(sockfd,infomassage.c_str(),infomassage.size());
           
           
            
            std::cout << "Server Echo>>> " <<infomassage << std::endl;

          }
          else
          {
            break;
          }
         
}



}

~Tcpserver()
{

}
private:
    int sockfd_;     // 网路文件描述符
    std::string ip_; // 任意地址bind 0
    uint16_t port_;  // 表明服务器进程的端口号

};

后面我们会用一个计算机串联一切知识点。

守护进程

有一种进程他会残留信息造成进程信息,称之为僵尸进程。有一种暖心的进程叫做守护进程。
这两种是同一种进程的不同翻译,是特殊的孤儿进程,不但运行在后台,最主要的是脱离了与终端和登录会话的所有联系,也就是默默的运行在后台不想受到任何影响,并且退出后不会成为僵尸进程。
进程关系图
在这里插入图片描述
前后台进程
用户登录时会建立一个会话,会话内部会构建一个前台进程组 和 0个或者多个后台进程组,linux下客户端登录时 会给我们加载bash,bash就是前台进程组。(windows下的注销就是新建立一个会话)前台进程组必须有一个,而且任何时刻只能有一个。
在这里插入图片描述
2. 守护进程的创建
守护进程的创建分两步:

  1. fork创建子进程。
  2. 父进程退出,并且调用setsid()函数接口。

必做:fork+setsid()——让自己不成为进程组组长+设置自己是一个独立的会话
那我如何不成为组长以便调用setsid呢?——bash中新启动第一个进程一定成为组长,所以你可以成为进程组内的第二个进程。即:常规做法:fork()子进程,子进程就不再是组长进程了,它就可以成功调用setsid(); ————

if(fork() > 0) exit(0) ;
setsid() ;        

改守护进程的工作目录,如何更改进程的工作目录?—chdir()

(3)一般守护进程都要做的(必做):

  1. (不常用做法一)因为守护进程与标准输入,标准输出,标准错误已经没关系了,所以close(0, 1,2) 守护进程获取输入或写入都是和网络有关,不会从键盘获取,不会往显示器输出。(很少有人这样做,因为兼容性不好,会导致代码中的打印代码报错)

守护进程作为后台进程,就不能把自己的输出排放到显示器上。所以,他应该把产生的一切信息写入垃圾箱。
类似于所有Linux下的一个”垃圾桶(文件黑洞)“,凡是从 /dev/null 里面读/写一概被丢弃

推荐做法:打开/dev/null, 并且对 0,1,2 进行重定向!

总结:1.忽略SIGPIPE
2.更改进程的工作目录
3.让自己不要成为进程组组长(必做)
4.设置自己是一个独立的会话(必做)
5.重定向0,1,2(必做)

计算器

该计算器具有

  1. 多线程(生成消费者模型) 2. 守护进程 3. 打印日志 4. 计算数据四个功能。并且该计算机的协议自己手动实现。使用TCP/IP协议。

模块1 日志头文件

#include <iostream>
#include <time.h>
#include <stdarg.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>
#include <fstream> // 如果您打算使用 C++ 的文件流  
#include <cstring> // 如果您使用 C 风格的字符串操作 
#include <errno.h>
#include <cerrno>
#define Info 0
#define Debug 1
#define Warning 2
#define Error 3
#define Fatal 4

#define Screen 1
#define Onefile 2
#define Classfile 3
using namespace std;
#define FILENAME "log.txt"

#define SIZE 1024
class Log
{
public:
Log(int printMethod=Screen,std::string path="./log/")//构造函数
:_path(path),_printMethod(printMethod)
{

}
//错误等级 输入错误等级返回字符串
std::string LevelTostring(int level)
{
switch (level)
        {
        case Info:
            return "Info";
        case Debug:
            return "Debug";
        case Warning:
            return "Warning";
        case Error:
            return "Error";
        case Fatal:
            return "Fatal";
        default:
            return "None";
        }
}
void printLog(int level, const std::string &logtxt)  
{  
    // 根据 printMethod 的值选择不同的日志输出方式  
    switch (_printMethod)  
    {  
        // 如果 printMethod 为 Screen,则将日志内容输出到屏幕  
    case Screen:  
        std::cout << logtxt << std::endl;  
        break;  
  
        // 如果 printMethod 为 Onefile,则将日志内容输出到指定的日志文件中  
    case Onefile:  
        printOneFile(FILENAME,logtxt);  
        break;  
  
        // 如果 printMethod 为 Classfile,则根据日志级别和日志内容输出到分类的日志文件中  
    case Classfile:  
        printClassFile(level, logtxt);  
        break;  
  
        // 如果 printMethod 的值不是上述任何一种,则不执行任何操作  
    default:  
        break;  
    }  
}

 void printOneFile(const std::string &Filepanth,const std::string &Filetxt ) 
 {
//文件在系统中的路径
  std::string _Filepanth=_path+Filepanth;
  //可读可写方式创建log.txt
int fd = open(_Filepanth.c_str(), O_WRONLY | O_CREAT | O_APPEND, 0666); // "log.txt"
if(fd<0)
{
    return ;
}
write(fd,Filetxt.c_str(),Filetxt.size());

 }
void printClassFile(int level,const std::string Filetxt)
{
    std::string _Filepanth =_path+ LevelTostring(level)+"/"+FILENAME;
   // cout<<_Filepanth<<endl;
    int fd = open(_Filepanth.c_str(), O_WRONLY | O_CREAT | O_APPEND, 0666); // "log.txt"
//cout<<fd<<endl;
if(fd<0)
{
     perror("Error opening file"); // 这将打印一个描述错误的消息 
    return ;
}
std:: string tmp("\n");
std::string Filetxttmp=Filetxt+tmp;
write(fd,Filetxttmp.c_str(),Filetxttmp.size());
} 
 void operator()(int level, const char *format, ...)
    {
        time_t t = time(nullptr);
        struct tm *ctime = localtime(&t);
        char leftbuffer[SIZE];
        snprintf(leftbuffer, sizeof(leftbuffer), "[%s][%d-%d-%d %d:%d:%d]", LevelTostring(level).c_str(),
                 ctime->tm_year + 1900, ctime->tm_mon + 1, ctime->tm_mday,
                 ctime->tm_hour, ctime->tm_min, ctime->tm_sec);

        va_list s;
        va_start(s, format);
        char rightbuffer[SIZE];
        vsnprintf(rightbuffer, sizeof(rightbuffer), format, s);
        int len=strlen(rightbuffer);
        rightbuffer[len]='\n';
        va_end(s);

        // 格式:默认部分+自定义部分
        char logtxt[SIZE * 2];
        snprintf(logtxt, sizeof(logtxt), "%s %s", leftbuffer, rightbuffer);

        // printf("%s", logtxt); // 暂时打印
        printLog(level, logtxt);
    }
~Log()
{

}

private:

std::string _path;//文件所在路径
int _printMethod;//打印方法
};



序列化和反序列化

序列化和反序列化 序列化和反序列化是保证数据的完整性的工作。
直接发送同样的结构体对象,是不可取的,虽然在某些情况下,它确实行,但是我们需要进行序列化和反序列化

定义:定义结构体来表示我们需要交互的信息 ; 发送数据时将这个结构体按照一个规则转换成字符串, 接收到数据的时候再按照相同的规则把字符串转化回结构体; 这个过程叫做 " 序列化 " 和 " 反序列化 "
序列化,反序列化的操作是在用户发送和网络发送中间加了一层软件层,
———————————————————————
序列化和反序列化的示意图:
在这里插入图片描述

  • 7
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值