网络字节序——TCP接口及其实现简单TCP服务器

news2024/9/23 23:29:58

网络字节序——TCP接口及其实现简单TCP服务器

森格 (2)

文章目录

  • 网络字节序——TCP接口及其实现简单TCP服务器
      • 简单TCP服务器的实现
        • 1. 单进程版:客户端串行版
        • 2. 多进程版:客户端并行版
        • netstat查看网络信息
        • 3.多线程版:并行执行
        • log.hpp
      • 守护进程
          • fg、bg
        • setsid

简单TCP服务器的实现

  • TCP区别于UDP在于要设置套接字为监控状态,即TCP是面向链接,因此TCP套接字需要设置为监听状态
void initserver()
{
//1.创建套接字
_listensock=socket(AF_INET,SOCK_STREAM,0);
if(_listensock<0)
{
    logMessage(FATAL,"create listensocket error");
    exit(SOCK_ERR);
}
 logMessage(NORMAL, "create socket success: %d", _listensock);
//2.bind ip和port
struct sockaddr_in local;
local.sin_family=AF_INET;
local.sin_port=htons(_port);
local.sin_addr.s_addr=INADDR_ANY;
if(bind(_listensock,(struct sockaddr*)&local,sizeof(local))<0)//绑定失败
{
    logMessage(FATAL,"bind error");
    exit(BIND_ERR);
}
 logMessage(NORMAL,"bind success");
//3.将套接字设置为监听模式
if(listen(_listensock,0)<0)
{
    logMessage(FATAL,"listen error");
    exit(LISTEN_ERR);
}
logMessage(NORMAL,"listen success");
}

socket函数原型

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

int socket(int domain, int type, int protocol);
  • domain 表示协议族,常用的有 AF_INET(IPv4)和 AF_INET6(IPv6)。

  • type 表示Socket类型,常用的有 SOCK_STREAM(TCP)和 SOCK_DGRAM(UDP)。

  • protocol 通常可以设置为 0,让系统根据 domaintype 来选择合适的协议。

  • socket()打开一个网络通讯端口,如果成功的话,就像open()一样返回一个文件描述符

  • 应用程序可以像读写文件一样通过socket函数用read/write在网络上收发数据

bind函数原型

int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
  • sockfd 是socket描述符。

  • addr 是一个 struct sockaddr 结构体,包含要绑定的IP地址和端口信息。

  • addrlenaddr 结构体的长度。因为addr结构体可以接受多种协议的sockaddr结构体,因此要传其结构体的长度

  • bind()成功返回0,失败返回-1。

  • bind()的作用是将参数sockfd和myaddr绑定在一起, 使sockfd这个用于网络通讯的文件描述符监听addr所描述的地址和端口号;

listen函数原型

int listen(int sockfd, int backlog);
  • sockfd 是socket描述符,指用于进行网络监听的文件描述符
  • backlog 表示等待连接队列的最大长度。
  • listen成功返回0,失败返回-1
  • listen函数将使得sockfd处于监听状态,并且允许backlog个客户端处于连接等待状态,当收到多于backlog个客户端的的连接请求则选择忽略。
  • 实际上listen函数告诉操作系统指定的套接字sockfd处于监听状态,该套接字开始等待其他计算机通过网络与其建立连接,一旦有连接请求到达,操作系统会将连接请求放入连接队列中,连接队列的最大长度为backlog,连接队列是一个存放连接请求的缓冲区,如果队列已满新的连接请求将会被拒绝。即当一个套接字处于监听状态时,它不直接处理数据传输,而是等待其他计算机发起连接。

总的来说initserver函数作用是先创建套接字,然后填充指定的端口号和ip,并将套接字设置为监听状态

void start()
{
    while(true)
    {
        struct sockaddr_in cli;
        socklen_t len=sizeof(cli);
        bzero(&cli,len);

        int sock=accept(_listensock,(struct sockaddr*)&cli,&len);
        if(sock<0)
        {
            logMessage(FATAL,"accept client error");
            continue;
        }
        logMessage(NORMAL,"accept client success");

        cout<<"accept sock: "<<sock<<endl;
        }

accept函数原型

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

int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
  • sockfd:是一个已经通过 socket 函数创建的套接字描述符,并且是已经处于监听状态,用于监听传入的连接请求。

  • addr:是一个指向 struct sockaddr 结构的指针,用于接收连接请求的客户端的地址信息。

  • addrlen:是一个指向 socklen_t 类型的指针,用于指定 addr 缓冲区的长度,同时也用于返回实际客户端地址结构的大小。

  • accept函数作用是接受传入的连接请求,他会阻塞程序的执行,直到有一个连接请求到达。一旦有连接请求到达,将会创建一个新的套接字,并返回这个新套接字的文件描述符,这个新套接字用于与客户端进行通信,同时addraddrlen会填充上客户端的地址信息。

  • 在服务器程序中,accept函数会被用在一个循环中,以接受多个客户端的连接请求

start函数作用是阻塞接受客户端发送来的连接请求,使得服务器与客户端建立通信

tcpclient.cc

#include<iostream>
#include<string>
#include<memory>
#include"tcpclient.hpp"
using namespace std;
using namespace client;
static void Usage(string proc)
{
    cout<<"\nUsage :\n\t"<<proc<<" serverip serverport\n"<<endl;
}
int main(int argc, char* argv[])
{
    if(argc!=3)
    {
        Usage(argv[0]);
        exit(1);
    }

string serverip=argv[1];
uint16_t serverport=atoi(argv[2]);

unique_ptr<tcpclient> tc(new tcpclient(serverip,serverport));

tc->initclient();
tc->start();

    return 0;
}

tcpclient.hpp

#pragma once
#include <iostream>
#include <string>
#include <cstring>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
using namespace std;
#define NUM 1024
namespace client
{

    class tcpclient
{

public:
tcpclient(const string& ip,const uint16_t& port)
:_sock(-1)
,_port(port)
,_ip(ip)
{}

void initclient()
{
//1.创建sockfd
_sock=socket(AF_INET,SOCK_STREAM,0);
if(_sock<0)
{
   cerr<<"socket create error"<<endl;
   exit(2);
}
//2.绑定 ip port,不显示绑定,OS自动绑定
}

void start()
{
struct sockaddr_in ser;
bzero(&ser,sizeof(ser));
socklen_t len=sizeof(ser);
ser.sin_family=AF_INET;
ser.sin_port=htons(_port);
ser.sin_addr.s_addr=inet_addr(_ip.c_str());
if(connect(_sock,(struct sockaddr *)&ser,len)!=0)
{
    cerr<<"connect error"<<endl;
}else
{
    string msg;
    while(true)
    {
        cout<<"Enter# ";
        getline(cin,msg);
        write(_sock,msg.c_str(),msg.size());
        
        char inbuffer[NUM];
        int n=read(_sock,inbuffer,sizeof(inbuffer)-1);
        if(n>0)
        {
            cout<<"server return :"<<inbuffer<<endl;
        }else
        {
            break;
        }
    }
}
}

~tcpclient()
{
    if(_sock>=0) close(_sock);
}

private:
int _sock;
uint16_t _port;
string _ip;

};
}

tcpserver.cc

#include"tcpserver.hpp"
#include"log.hpp"
#include<iostream>
#include<stdlib.h>
#include<memory>
using namespace Server;
using namespace std;

static void Usage(string proc)
{
    cout<<"\nUsage:\n\t"<<proc<<" local_port\n\n"<<endl;
}

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

uint16_t port=atoi(argv[1]);//将字符串转化为整数

unique_ptr<tcpserver> ts(new tcpserver(port));
ts->initserver();
ts->start();


return 0;
}

tcpserver.hpp

#pragma once
#include <iostream>
#include <string>
#include <cstring>
#include <cstdlib>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/wait.h>
#include <signal.h>
#include <pthread.h>
#include"log.hpp"
#define NUM 1024

using namespace std;
namespace Server
{
    enum
    {
        USAGE_ERR=1,SOCK_ERR,BIND_ERR,LISTEN_ERR
    };
class tcpserver;
    class ThreadData
    {
public:
ThreadData( tcpserver* self,int psock):_this(self),_psock(psock){}
tcpserver* _this;
int _psock;
    };

class tcpserver
{
 
public:

tcpserver(const  uint16_t& port):_port(port),_listensock(-1){}

void initserver()
{
//1.创建套接字
_listensock=socket(AF_INET,SOCK_STREAM,0);
if(_listensock<0)
{
    logMessage(FATAL,"create listensocket error");
    exit(SOCK_ERR);
}
 logMessage(NORMAL, "create socket success: %d", _listensock);
//2.bind ip和port
struct sockaddr_in local;
local.sin_family=AF_INET;
local.sin_port=htons(_port);
local.sin_addr.s_addr=INADDR_ANY;
if(bind(_listensock,(struct sockaddr*)&local,sizeof(local))<0)//绑定失败
{
    logMessage(FATAL,"bind error");
    exit(BIND_ERR);
}
 logMessage(NORMAL,"bind success");
//3.将套接字设置为监听模式
if(listen(_listensock,0)<0)
{
    logMessage(FATAL,"listen error");
    exit(LISTEN_ERR);
}
logMessage(NORMAL,"listen success");
}

void start()
{
     // signal(SIGCHLD, SIG_IGN);
    threadPool<Task>::getthpptr()->run();
    while(true)
    {
        struct sockaddr_in cli;
        socklen_t len=sizeof(cli);
        bzero(&cli,len);

        int sock=accept(_listensock,(struct sockaddr*)&cli,&len);
        if(sock<0)
        {
            logMessage(FATAL,"accept client error");
            continue;
        }
        logMessage(NORMAL,"accept client success");

        cout<<"accept sock: "<<sock<<endl;
        // serviceIO(sock);//客户端串行版
        // close(sock);

        //多进程版---
        //一个客户端占用一个文件描述符,原因在于孙子进程执行IO任务需要占用独立的文件描述符,而文件描述符是继承父进程的,而每次客户端进来都要占用新的文件描述符
        //因此若接收多个客户端不退出的话文件描述符会越来越少。
//         pid_t id=fork();//创建子进程
//         if(id==0)//子进程进入
//         {
//             close(_listensock);//子进程不需要用于监听因此关闭该文件描述符
//             if(fork()>0)  exit(0);
// //子进程创建孙子进程,子进程直接退出,让孙子进程担任IO任务,且孙子进程成为孤儿进程被OS领养,
// //除非客户端退出IO任务结束否则该孤儿进程一直运行下去不会相互干扰,即并行完成服务器和客户端的通信

// //孙子进程
// serviceIO(sock);
// close(sock);
// exit(0);
//         }
//         //父进程
//         pid_t ret=waitpid(id,nullptr,0);
//         if(ret<0)
//         {
//             cout << "waitsuccess: " << ret << endl;
//         }

//多线程版
// pthread_t pid;
// ThreadData* th=new ThreadData(this,sock);
// pthread_create(&pid,nullptr,start_routine,th);

threadPool<Task>::getthpptr()->push(Task(sock,serviceIO));
    }
}

// static void* start_routine(void* args)
// {
//     pthread_detach(pthread_self());
//     ThreadData* ret=static_cast<ThreadData*>(args);
//     ret->_this->serviceIO(ret->_psock);
//     close(ret->_psock);
//     delete ret;
//     return nullptr;
// } 

// void serviceIO(int sock)
// {
//     char inbuffer[NUM];
//     while(true)
//     {
//         ssize_t n=read(sock,inbuffer,sizeof(inbuffer)-1);
//         if(n>0)
//         {
//             inbuffer[n]=0;
//             cout<<"recv message: "<<inbuffer<<endl;
//             string outb=inbuffer;
//             string outbuffer=outb+"[server echo]";

//             write(sock,outbuffer.c_str(),outbuffer.size());

//         }
// else
// {
//     logMessage(NORMAL,"client quit,i quit yep");
//     break;
// }
//     }

// }

~tcpserver(){}

private:
int _listensock;//用于监听服务器的sock文件描述符
uint16_t _port;//端口号
};

}

1. 单进程版:客户端串行版

#pragma once
#include <iostream>
#include <string>
#include <cstring>
#include <cstdlib>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/wait.h>
#include <signal.h>
#include <pthread.h>
#include"log.hpp"
#define NUM 1024

using namespace std;
namespace Server
{
    enum
    {
        USAGE_ERR=1,SOCK_ERR,BIND_ERR,LISTEN_ERR
    };

class tcpserver
{
 
public:

tcpserver(const  uint16_t& port):_port(port),_listensock(-1){}

void initserver()
{
//1.创建套接字
_listensock=socket(AF_INET,SOCK_STREAM,0);
if(_listensock<0)
{
    logMessage(FATAL,"create listensocket error");
    exit(SOCK_ERR);
}
 logMessage(NORMAL, "create socket success: %d", _listensock);
//2.bind ip和port
struct sockaddr_in local;
local.sin_family=AF_INET;
local.sin_port=htons(_port);
local.sin_addr.s_addr=INADDR_ANY;
if(bind(_listensock,(struct sockaddr*)&local,sizeof(local))<0)//绑定失败
{
    logMessage(FATAL,"bind error");
    exit(BIND_ERR);
}
 logMessage(NORMAL,"bind success");
//3.将套接字设置为监听模式
if(listen(_listensock,0)<0)
{
    logMessage(FATAL,"listen error");
    exit(LISTEN_ERR);
}
logMessage(NORMAL,"listen success");
}

void start()
{
    while(true)
    {
        struct sockaddr_in cli;
        socklen_t len=sizeof(cli);
        bzero(&cli,len);

        int sock=accept(_listensock,(struct sockaddr*)&cli,&len);
        if(sock<0)
        {
            logMessage(FATAL,"accept client error");
            continue;
        }
        logMessage(NORMAL,"accept client success");

        cout<<"accept sock: "<<sock<<endl;
         serviceIO(sock);//客户端串行版
         close(sock);
    }
}


void serviceIO(int sock)
{
    char inbuffer[NUM];
    while(true)
    {
        ssize_t n=read(sock,inbuffer,sizeof(inbuffer)-1);
        if(n>0)
        {
            inbuffer[n]=0;
            cout<<"recv message: "<<inbuffer<<endl;
            string outb=inbuffer;
            string outbuffer=outb+"[server echo]";

            write(sock,outbuffer.c_str(),outbuffer.size());

        }
else
{
    logMessage(NORMAL,"client quit,i quit yep");
    break;
}
    }

}

~tcpserver(){}

private:
int _listensock;//用于监听服务器的sock文件描述符
uint16_t _port;//端口号
};

}

注意:客户端串行給服务器发数据是在哪里堵塞?由于阻塞在accept函数处,即accept等待客户端接入是阻塞式等待。accept函数接收了一个连接请求后,后来的客户端连接请求需要在accept函数处等待,当上一个客户端退出后,服务器才能accept当前客户端发送来的连接请求成功,才能接收当前客户端的数据。即服务器串行接收处理客户端发送来的数据

2. 多进程版:客户端并行版

tcpserver.hpp

#pragma once
#include <iostream>
#include <string>
#include <cstring>
#include <cstdlib>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/wait.h>
#include <signal.h>
#include <pthread.h>
#include"log.hpp"
#define NUM 1024

using namespace std;
namespace Server
{
    enum
    {
        USAGE_ERR=1,SOCK_ERR,BIND_ERR,LISTEN_ERR
    };
class tcpserver
{
 
public:

tcpserver(const  uint16_t& port):_port(port),_listensock(-1){}

void initserver()
{
//1.创建套接字
_listensock=socket(AF_INET,SOCK_STREAM,0);
if(_listensock<0)
{
    logMessage(FATAL,"create listensocket error");
    exit(SOCK_ERR);
}
 logMessage(NORMAL, "create socket success: %d", _listensock);
//2.bind ip和port
struct sockaddr_in local;
local.sin_family=AF_INET;
local.sin_port=htons(_port);
local.sin_addr.s_addr=INADDR_ANY;
if(bind(_listensock,(struct sockaddr*)&local,sizeof(local))<0)//绑定失败
{
    logMessage(FATAL,"bind error");
    exit(BIND_ERR);
}
 logMessage(NORMAL,"bind success");
//3.将套接字设置为监听模式
if(listen(_listensock,0)<0)
{
    logMessage(FATAL,"listen error");
    exit(LISTEN_ERR);
}
logMessage(NORMAL,"listen success");
}

void start()
{
     // signal(SIGCHLD, SIG_IGN);
    while(true)
    {
        struct sockaddr_in cli;
        socklen_t len=sizeof(cli);
        bzero(&cli,len);

        int sock=accept(_listensock,(struct sockaddr*)&cli,&len);
        if(sock<0)
        {
            logMessage(FATAL,"accept client error");
            continue;
        }
        logMessage(NORMAL,"accept client success");

        cout<<"accept sock: "<<sock<<endl;

                //多进程版---
        //一个客户端占用一个文件描述符,原因在于孙子进程执行IO任务需要占用独立的文件描述符,而文件描述符是继承父进程的,而每次客户端进来都要占用新的文件描述符
        //因此若接收多个客户端不退出的话文件描述符会越来越少。
        pid_t id=fork();//创建子进程
        if(id==0)//子进程进入
        {
            close(_listensock);//子进程不需要用于监听因此关闭该文件描述符
            if(fork()>0)  exit(0);
// //子进程创建孙子进程,子进程直接退出,让孙子进程担任IO任务,且孙子进程成为孤儿进程被OS领养,
// //除非客户端退出IO任务结束否则该孤儿进程一直运行下去不会相互干扰,即并行完成服务器和客户端的通信

// //孙子进程
serviceIO(sock);
close(sock);
exit(0);
        }
        //父进程
         // close(sock);//父进程不使用文件描述符就关闭
        pid_t ret=waitpid(id,nullptr,0);
        if(ret<0)
        {
            cout << "waitsuccess: " << ret << endl;
        }
    }
}
void serviceIO(int sock)
{
    char inbuffer[NUM];
    while(true)
    {
        ssize_t n=read(sock,inbuffer,sizeof(inbuffer)-1);
        if(n>0)
        {
            inbuffer[n]=0;
            cout<<"recv message: "<<inbuffer<<endl;
            string outb=inbuffer;
            string outbuffer=outb+"[server echo]";

            write(sock,outbuffer.c_str(),outbuffer.size());

        }
else
{
    logMessage(NORMAL,"client quit,i quit yep");
    break;
}
    }

}
~tcpserver(){}
private:
int _listensock;//用于监听服务器的sock文件描述符
uint16_t _port;//端口号
};

}
  • 父进程fork创建子进程,创建完后waitpid等待回收子进程。子进程fork创建孙子进程,创建完后直接退出。导致孙子进程成为孤儿进程,进而被OS领养。因此除非客户端退出IO任务,否则孤儿进程将一直运行下去不会干扰到其他进程,即并行完成服务器和客户端的通信

  • 注意的是服务器accept一次客户端的连接请求,就需要申请一个文件描述符,而文件描述符是有上限的,如果大量的客户端请求连接成功并且不结束的话,会造成文件描述符泄露。

image-20230825163937980

因此在父进程那里需要关闭不使用的文件描述符

image-20230825192206974

  • 父进程这里回收子进程,不能使用非阻塞等待,原因在于非阻塞等待的本质是轮询,而这里使用后会导致父进程会在accept函数处阻塞等待客户端发送连接请求,那么父进程就无法回收子进程了。因此waitpid的返回值用ret接收,等待回收成功就打印日志,失败则跳过
  • 当子进程图退出或者被中止时子进程会发送17号信号SIGCHILD给父进程,父进程可以通过忽略17号信号SIGCHILD的方式来不阻塞等待回收子进程(这种方法对于linux环境可用,其余不保证)
signal(SIGCHLD, SIG_IGN);

netstat查看网络信息

netstat 是一个用于查看网络连接和网络统计信息的命令行工具。它可以用来显示当前系统上的网络连接、路由表、接口统计信息等等。在 Linux 系统中,netstat 命令的用法如下:

netstat [options]

一些常用的选项包括:

  • -a:显示所有的连接,包括监听中和已建立的连接。
  • -t:显示 TCP 协议的连接。
  • -u:显示 UDP 协议的连接。
  • -n:以数字形式显示 IP 地址和端口号,而不是尝试进行 DNS 解析。
  • -p:显示与连接关联的进程信息。
  • -r:显示路由表。
  • -l:仅显示监听中的连接。
  • -atun:显示所有的TCP和UDP连接

image-20230825190401543

注意一下:这里出现了两个连接,原因在于服务器和客户端在同一台主机上,即服务器和客户端完成了本地环回,因此能看到两个连接。

3.多线程版:并行执行

tcpserver.hpp

#pragma once
#include <iostream>
#include <string>
#include <cstring>
#include <cstdlib>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/wait.h>
#include <signal.h>
#include <pthread.h>
#include"log.hpp"
#define NUM 1024

using namespace std;
namespace Server
{
    enum
    {
        USAGE_ERR=1,SOCK_ERR,BIND_ERR,LISTEN_ERR
    };
class tcpserver;
    class ThreadData
    {
public:
ThreadData( tcpserver* self,int psock):_this(self),_psock(psock){}
tcpserver* _this;
int _psock;
    };

class tcpserver
{
 
public:

tcpserver(const  uint16_t& port):_port(port),_listensock(-1){}

void initserver()
{
//1.创建套接字
_listensock=socket(AF_INET,SOCK_STREAM,0);
if(_listensock<0)
{
    logMessage(FATAL,"create listensocket error");
    exit(SOCK_ERR);
}
 logMessage(NORMAL, "create socket success: %d", _listensock);
//2.bind ip和port
struct sockaddr_in local;
local.sin_family=AF_INET;
local.sin_port=htons(_port);
local.sin_addr.s_addr=INADDR_ANY;
if(bind(_listensock,(struct sockaddr*)&local,sizeof(local))<0)//绑定失败
{
    logMessage(FATAL,"bind error");
    exit(BIND_ERR);
}
 logMessage(NORMAL,"bind success");
//3.将套接字设置为监听模式
if(listen(_listensock,0)<0)
{
    logMessage(FATAL,"listen error");
    exit(LISTEN_ERR);
}
logMessage(NORMAL,"listen success");
}

void start()
{
    while(true)
    {
        struct sockaddr_in cli;
        socklen_t len=sizeof(cli);
        bzero(&cli,len);

        int sock=accept(_listensock,(struct sockaddr*)&cli,&len);
        if(sock<0)
        {
            logMessage(FATAL,"accept client error");
            continue;
        }
        logMessage(NORMAL,"accept client success");

        cout<<"accept sock: "<<sock<<endl;
        //多线程版
pthread_t pid;
ThreadData* th=new ThreadData(this,sock);
pthread_create(&pid,nullptr,start_routine,th);
    }
}
 static void* start_routine(void* args)
{
    pthread_detach(pthread_self());//线程分离后让OS自动回收新线程
    ThreadData* ret=static_cast<ThreadData*>(args);
    ret->_this->serviceIO(ret->_psock);
    close(ret->_psock);
    delete ret;
    return nullptr;
}    
    
void serviceIO(int sock)
{
    char inbuffer[NUM];
    while(true)
    {
        ssize_t n=read(sock,inbuffer,sizeof(inbuffer)-1);
        if(n>0)
        {
            inbuffer[n]=0;
            cout<<"recv message: "<<inbuffer<<endl;
            string outb=inbuffer;
            string outbuffer=outb+"[server echo]";

            write(sock,outbuffer.c_str(),outbuffer.size());

        }
else
{
    logMessage(NORMAL,"client quit,i quit yep");
    break;
}
    }

}
~tcpserver(){}
private:
int _listensock;//用于监听服务器的sock文件描述符
uint16_t _port;//端口号
};

}
  • 服务器接收一个客户端的连接请求,就申请一个新线程,多线程下可以让服务器接收多个线程

log.hpp

#pragma once

#include <iostream>
#include <string>
#include<ctime>
#include <sys/types.h>
 #include <unistd.h>
 #include <stdio.h>
#include <stdarg.h>
using namespace std;
#define DEBUG   0
#define NORMAL  1
#define WARNING 2
#define ERROR   3
#define FATAL   4

#define NUM 1024
#define LOG_STR "./logstr.txt"
#define LOG_ERR "./log.err"
const char* to_str(int level)
{
    switch(level)
    {
        case DEBUG: return "DEBUG";
        case NORMAL: return "NORMAL";
        case WARNING: return "WARNING";
        case ERROR: return "ERROR";
        case FATAL: return "FATAL";
        default: return nullptr;
    }
}

void logMessage(int level, const char* format,...)
{
    // [日志等级] [时间戳/时间] [pid] [messge]
    // [WARNING] [2023-05-11 18:09:08] [123] [创建socket失败]

    // 暂定
  //  std::cout << message << std::endl;

char logprestr[NUM];
snprintf(logprestr,sizeof(logprestr),"[%s][%ld][%d]",to_str(level),(long int)time(nullptr),getpid());//把后面的内容打印进logprestr缓存区中

char logeldstr[NUM];
va_list arg;
va_start(arg,format); 
vsnprintf(logeldstr,sizeof(logeldstr),format,arg);//arg是logmessage函数列表中的...

  cout<<logprestr<<logeldstr<<endl;

//  FILE* str=fopen(LOG_STR,"a");
//  FILE* err=fopen(LOG_ERR,"a");//以追加方式打开文件,若文件不存在则创建
 
//  if(str!=nullptr||err!=nullptr)//两个文件指针都不为空则创建文件成功
//  {
//   FILE* ptr=nullptr;
//   if(level==DEBUG||level==NORMAL||level==WARNING)
//   {
//     ptr=str;
//   }
//    if(level==ERROR||level==FATAL)
//   {
//     ptr=err;
//   }
//   if(ptr!=nullptr)
//   {
//     fprintf(ptr,"%s-%s\n",logprestr,logeldstr);
//   }
//   fclose(str);
//   fclose(err);
 //}

}

可变参数列表

  • va_list是(char*)重命名的类型,定义可以访问可变参数的变量。

  • va_start(ap, v) ap是定义的可变参数变量,v是形参中可变参数前第一个参数名,其作用是使ap指向可变参数部分。

  • va_arg(ap, t) ap是定义的可变参数变量,t是可变参数的类型,根据类型,访问可变参数列表中的数据。

  • va_end(ap) ap是定义的可变参数变量,使ap变量置空,作为结束使用。

vsnprintf函数原型

#include <stdio.h>
int vsnprintf(char *str, size_t size, const char *format, va_list ap);
  • str是一个指向字符数组(缓冲区)的指针,用于存储格式化后的数据

  • size是缓冲区的大小,限制了写入的最大字符数,包括终止的 null 字符

  • format格式化字符串,类似于 printf 函数中的格式化字符串

  • ap是一个 va_list 类型的变量,用于存储可变参数列表的信息,并且要注意OS对参数压栈的顺序是从右向左

  • vsnprintf 函数根据指定的 format 格式化字符串将数据写入 str 缓冲区,但不会超出指定的缓冲区大小。它会在写入数据后自动在缓冲区末尾添加一个 null 终止字符,确保结果是一个合法的 C 字符串。

image-20230825224540368

守护进程

守护进程(Daemon)是在计算机系统中以后台方式运行的一类特殊进程。它通常在操作系统启动时被初始化,并在整个系统运行期间保持活动状态,不需要与用户交互。守护进程通常用于执行系统任务、服务管理以及提供后台服务,如网络服务、定时任务等。

守护进程特点如下:

  1. 后台运行,守护进程在后台运行,不与用户交互,没有控制终端。
  2. 独立性:它通常独立于用户会话,即使用户注销或关闭终端,守护进程也会继续运行。
  3. 没有标准输入输出:守护进程通常没有标准输入和输出,因为它们不与用户交互。它们通常将输出写入日志文件。
  4. 分离自身:守护进程会通过一系列操作来与终端、会话和控制组脱离连接,以确保它不会意外地被控制终端关闭。

一个服务器中可以具有多个会话,例如一个服务器上有一个root用户和多个普通用户,当普通用户登录上服务器时即成为一个会话。

一个会话具有多个后台任务,但只能具有一个前台任务(bash)。

image-20230825231612464

  • jobs查看任务可以看到任务1是./tcpserver,任务2是sleep 1000 | sleep 2000 | sleep 3000 &,任务3是sleep 4000 | sleep 5000 &,且三个任务后面都带&,在进程或任务后带&作用是将该任务放到后台运行
  • sleep 1000 、sleep 2000 、sleep 3000 、sleep 4000、sleep 5000的父进程都是16853即bash;而 sleep 1000 、sleep 2000 、sleep 3000的PGID相同,都是sleep 1000的pid,即 sleep 1000 、sleep 2000 、sleep 3000属于同一组,同一个组要协同起来完成同一个作业。第一个任务的pid是组长的pid即sleep 1000的pid;而小组16858和小组17070的SID都是16853,即这两个小组属于同一个会话(bash),要完成的是同一个任务;
fg、bg
  1. fg 作业号:将作业放到前台

  2. bg 作业号:将作业放到后台,或者继续执行后台作业

  3. ctrl+Z将前台任务暂停并把作业放到后台

image-20230825233513024

  • 用户登录时服务器就需要为此创建一些后台作业和前台作业(命令行)来服务用户,而用户注销或退出服务器也会影响其前台作业和后台作业。而服务器程序不能受到用户登录和注销的影响。
  • 我们可以使得服务器程序自成会话,自成进程组,那么该程序就与终端设备无关,不能再收到用户登录和注销的影响了。该类进程被称为守护进程

setsid

在Unix和类Unix系统中,setsid 是一个用于创建新会话的系统调用函数。会话(Session)是一组相关的进程组合,通常由一个控制终端和一些子进程组成。setsid 函数的主要作用是将调用它的进程从当前会话中分离出来,并创建一个新的会话。

 #include <unistd.h>
pid_t setsid(void);
  • 创建新会话:调用 setsid 的进程会成为一个新的会话的组长(Session Leader)。新会话不再与之前的控制终端相关联。但该进程在调用setsid函数之前不能是组长。
  • 分离终端:调用 setsid 的进程不再与任何控制终端关联,无法重新获得控制终端。
  • 成为新进程组的组长:新会话中的第一个进程(调用 setsid 的进程)会成为新的进程组的组长。

daemon.hpp

#pragma once

#include <unistd.h>
#include <signal.h>
#include <cstdlib>
#include <cassert>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#define DEV "/dev/null"
void daemonSelf(const char *currPath = nullptr)
{
    // 1. 让调用进程忽略掉异常的信号
signal(SIGPIPE,SIG_IGN);//选择忽略SIGPIPE信号
    // 2. 如何让自己不是组长,setsid
if(fork()>0)
exit(0);//父进程退出
    // 子进程 -- 守护进程,精灵进程,本质就是孤儿进程的一种!
pid_t ret=setsid();
assert(ret!=-1);
    // 3. 守护进程是脱离终端的,关闭或者重定向以前进程默认打开的文件
int fd=open(DEV,O_RDWR);
if(fd>=0)
{
    //dup2(oldfd,newfd):将oldfd的内容填充到newfd中,这样输入到newfd的内容被重定向到oldfd
    dup2(fd,0);
    dup2(fd,1);
    dup2(fd,2);
}else
{
    close(0);
    close(1);
    close(2);
}
    // 4. 可选:进程执行路径发生更改
if(currPath) chdir(currPath);//更改currPath的路径
}
  • /dev/null 是一个特殊的设备文件,它被用作数据丢弃点,向它写入的数据会被丢弃,从它读取数据会立即返回EOF(End of File)
  • SIGPIPE的触发场景:当一个进程向一个已经关闭写端的管道(或者套接字)写数据时、当进程向一个已经收到 RST 包(连接重置)的套接字发送数据时,该进程就会向父进程发送SIGPIPE信号来进行进程终止。对SIGPIPE进行忽略行为避免了进程向/dev/null中写入数据并出现错误导致的进程终止
  • 父进程创建子进程,父进程作为组长,父进程退出后,子进程能够自己成为组长即能够成为守护进程
  • dup2(oldfd,newfd):将oldfd的内容填充到newfd中,这样输入到newfd的内容被重定向到oldfd。在代码中是将输入文件描述符012的内容重定向到fd即/dev/null中

tcpserver.cc

#include"tcpserver.hpp"
#include"log.hpp"
#include"daemon.hpp"
#include<iostream>
#include<stdlib.h>
#include<memory>
using namespace Server;
using namespace std;

static void Usage(string proc)
{
    cout<<"\nUsage:\n\t"<<proc<<" local_port\n\n"<<endl;
}

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

uint16_t port=atoi(argv[1]);//将字符串转化为整数

unique_ptr<tcpserver> ts(new tcpserver(port));
ts->initserver();
daemonSelf();
ts->start();

return 0;
}

image-20230826112937206

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

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

相关文章

BI技巧丨Window应用之同环比

白茶曾介绍过OFFSET可以用来解决同环比的问题&#xff0c;其实微软最近推出的开窗函数WINDOW也可以用来解决同环比。 WINDOW函数基础语法 WINDOW ( from[, from_type], to[, to_type][, <relation>][, <orderBy>][, <blanks>][, <partitionBy>][, &l…

[论文分享]Skip-Attention: Improving Vision Transformers by Paying Less Attention

Skip-Attention: Improving Vision Transformers by Paying Less Attention 这项工作旨在提高视觉transformer&#xff08;ViT&#xff09;的效率。 虽然 ViT 在每一层都使用计算昂贵的自我注意操作&#xff0c;但我们发现这些操作在各层之间高度相关——这是导致不必要的计算的…

计算机网络-笔记-第二章-计算机网络概述

目录 二、第二章——物理层 1、物理层的基本概念 2、物理层下面的传输媒体 &#xff08;1&#xff09;光纤、同轴电缆、双绞线、电力线【导引型】 &#xff08;2&#xff09;无线电波、微波、红外线、可见光【非导引型】 &#xff08;3&#xff09;无线电【频谱的使用】 …

linux问题定位

1.CPU工作原理 2.Linux内存分配 3.栈 1&#xff09;.存储局部变量 函数参数 函数返回值的地方 2&#xff09;.每个线程的栈空间连续且相互独立 3&#xff09;.使用 x /100a $esp 可以看到栈内存中的原始数据 3.函数调用过程 函数调用过程在栈中如何组织数据的 4.堆 三级堆管理…

研磨设计模式day13组合模式

目录 场景 不用模式实现 代码实现 有何问题 解决方案 代码改造 组合模式优缺点 思考 何时选用 场景 不用模式实现 代码实现 叶子对象 package day14组合模式;/*** 叶子对象*/ public class Leaf {/*** 叶子对象的名字*/private String name "";/**…

KVM虚拟化平台安装及创建虚拟机

文章目录 一、KVM 简介二、安装KVM虚拟化平台1、方式一&#xff1a;安装操作系统时&#xff0c;添加虚拟化功能2、方式二&#xff1a;基于现有系统&#xff0c;安装虚拟化功能3、验证KVM安装是否无误 三、创建虚拟机1、创建虚拟机前环境准备工作2、创建CentOS7.5系统虚拟机 一、…

网工必备知识之——防火墙篇

目录 一、背景 二、类型 2.1.过滤防火墙 2.2.应用网关防火墙 2.3.服务防火墙 2.4.监控防火墙 三、功能 3.1.网络安全屏障 3.2.网络安全策略 3.3.进行监控审计 3.4.防止内部信息的外泄 四、五个安全域 五、三种工作模式 5.1交换模式(二层模式): 5.2路由模式(三层…

恶意软件分析和取证:深入研究各类恶意软件,讨论分析技术和数字取证方法,了解攻击者的行为和动机

章节一&#xff1a;引言 在当今数字化的世界中&#xff0c;恶意软件的威胁不断演化&#xff0c;给个人、企业 ja 尤其是政府带来了巨大的安全风险。恶意软件如病毒、木马、蠕虫等形式多样&#xff0c;攻击手段不断升级&#xff0c;导致了信息泄露、财产损失 ja 甚至国家安全的…

【深度学习】实验02 鸢尾花数据集分析

文章目录 鸢尾花数据集分析决策树K-means 鸢尾花数据集分析 决策树 # 导入机器学习相关库 from sklearn import datasets from sklearn import treeimport matplotlib.pyplot as plt import numpy as np# Iris数据集是常用的分类实验数据集&#xff0c; # 由Fisher, 1936收集…

CFC编程入门_【10分钟学会】

什么是CFC&#xff1a; 【差不多10分钟全学会】 CFC是图形化编程&#xff0c; 跟单片机的连线一样&#xff0c; 唯一的区别&#xff1a;功能块右侧是【只能输出】引脚。 只有左侧引脚可以输入输出。 有哪些控件&#xff1a; 指针&#xff1a;用于拖动功能块。 控制点&#xf…

C#将text文本中的单双行分开单独保存

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 文本的分割1.设定text文件的名称为02.文本导出 文本的分割 1.设定text文件的名称为0 代码如下&#xff1a; using System; using System.Collections.Generic; us…

【Seata】01 - Seata XA 模式 Demo 调用流程分析

文章目录 前言参考目录版本说明前置知识1、关于事务的理论知识2、关于 Seata3、Seata 领域模型 测试 Demo1、模块说明2、调用逻辑说明3、分析流程说明4、注意事项 Seata XA 模式 Commit 调用流程分析1、调用流程图2、XA 模式流程以及操作命令3、业务模块&#xff1a;流程入口3.…

掌握Six Sigma:逐步解锁业务流程优化的秘密之匙

一、Six Sigma方法简介 1. Six Sigma的起源和概念 Six Sigma起源于1980年代的摩托罗拉公司。当时的摩托罗拉在面临激烈的全球竞争和持续的质量问题时&#xff0c;发明了这种系统的管理方法&#xff0c;并通过实施&#xff0c;获得了显著的成绩。 所谓的“Six Sigma”&#x…

Tensorflow2.0搭建网络八股

目录 引言&#xff1a;keras与Tensorflow2.0结合 一、六步法 1.导入头文件&#xff1a;import 2.收集处理训练集和测试集&#xff1a;train, test&#xff1a; 3.描述各层网model tf.keras.models.Sequential&#xff1a; 4.描述使用什么优化反向传播&#xff1a;model.c…

Python中使用print()时如何实现不换行

平时刷题的时候大家可能会发现打印字符的时候需要你不换行才能得到正确答案&#xff0c;那么如何实现的。下面直接看例子。 使用print()函数时其实还有个默认的参数end&#xff0c;来看看具体怎么回事 list [a,b,c] for i in list:print(i)打印结果&#xff1a;在这里插入代…

【C/C++】父类指针指向子类对象 | 隐藏

创作不易&#xff0c;本篇文章如果帮助到了你&#xff0c;还请点赞 关注支持一下♡>&#x16966;<)!! 主页专栏有更多知识&#xff0c;如有疑问欢迎大家指正讨论&#xff0c;共同进步&#xff01; &#x1f525;c系列专栏&#xff1a;C/C零基础到精通 &#x1f525; 给大…

CleanMyMac最新版4.14Mac清理软件下载安装使用教程

苹果电脑是很多人喜欢使用的一种电脑&#xff0c;它有着优美的外观&#xff0c;流畅的操作系统&#xff0c;丰富的应用程序和高效的性能。但是&#xff0c;随着时间的推移&#xff0c;苹果电脑也会产生一些不必要的文件和数据&#xff0c;这些文件和数据就是我们常说的垃圾。那…

docker服务如何正确关停

说明&#xff1a;停止 docker 服务之前&#xff0c;先把所有的容器都停掉&#xff0c;如果没停掉&#xff0c;再重启 docker 服务之后&#xff0c;所有的容器就会成为 Exited 状态。如果你只是想停止 docker 服务&#xff0c;docker 中的容器还继续提供服务&#xff0c;需要在d…

NVIDIA DLI 深度学习基础 答案 领取证书

最后一节作业是水果分类的任务&#xff0c;一共6类&#xff0c;使用之前学习的知识在代码段上进行填空。 加载ImageNet预训练的基础模型 from tensorflow import kerasbase_model keras.applications.VGG16(weights"imagenet",input_shape(224, 224, 3),include_t…

首页搜索框传递参数,并在搜索页面中的搜索框中进行显示,搜索框绑定回车键进行搜索

实现搜索条件和搜索内容固定&#xff0c;以及回车键搜索跳转 1.写出搜索条件和搜索框 <form class"parent"><select id"searchSelect" style"border: 1px solid #325da7;border-right: none;" value"resource"><opt…