Linux:TCP和守护进程

一、TCP网络程序

1.1 TCP服务端

成员变量:

int _listensock; // 监听的文件描述符

string _ip; // 服务端ip

uint16_t _port; // 端口号

bool _isrunning; // 服务器是否在运行

1.1.1 InitServer-创建服务端

1、创建套接字socket

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

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

如果socket()调用出错则返回-1;

对于IPv4, family参数指定为AF_INET;

对于TCP协议,type参数指定为SOCK_STREAM, 表示面向流的传输协议

protocol参数的介绍从略,指定为0即可。

2、绑定套接字bind

服务器程序所监听的网络地址和端口号通常是固定不变的,客户端程序得知服务器程序的地址和端口号后 就可以向服务器发起连接; 服务器需要调用bind绑定一个固定的网络地址和端口号;

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

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

struct sockaddr *是一个通用指针类型,myaddr参数实际上可以接受多种协议的sockaddr结 构体,而它们的长度各不相同,所以需要第三个参数addrlen指定结构体的长度;

我们的程序中对myaddr参数是这样初始化的:

注意:其实大部分的接口都已经帮我们考虑到大小端的问题了,只不过ip和port需要我们写到OS里,所以需要我们自己去转化!

(1)将整个结构体清零;

(2)设置地址类型为AF_INET;

(3)网络地址为INADDR_ANY(或者是0.0.0.0), 这个宏表示本地的任意IP地址,因为服务器可能有多个网卡,每个网卡也可能绑 定多个IP地址, 这样设置可以在所有的IP地址上监听,直到与某个客户端建立了连接时才确定下来到底用哪个IP地址;

(4)端口号为SERV_PORT, 我们定义为8080;

这两步和之前UDP的基本一样

3、TCP是面向连接的,服务器一般都是一个比较被动的状态,要等待客户端和他建立连接关系。所以他需要不断保持一个监听的状态 listen

listen()声明sockfd处于监听状态, 并且最多允许有backlog个客户端处于连接等待状态, 如果接收到更多 的连接请求就忽略, 这里设置不会太大(一般是5),

listen()成功返回0,失败返回-1;

全部代码:

cpp 复制代码
void InitServer() // 创建服务器
  {
    // 1/创建套接字
    _listensock = socket(AF_INET, SOCK_STREAM, 0); // 面向字节流;
    if (_listensock < 0)
    {
      lg(Fatal, "create socket,errno:%d,errstring:%s", errno, strerror(errno));
      exit(SocketError);
    }
    lg(Info, "create socket success,_listsock:%d", _listensock);
    // 2/开始绑定
    struct sockaddr_in local;
    bzero(&local, sizeof(local)); // 先清空,然后再填进去
    local.sin_family = AF_INET;
    local.sin_port = htons(_port); // 转网络序列
    inet_aton(_ip.c_str(), &local.sin_addr);
    // 开始绑定
    if (bind(_listensock, (sockaddr *)&local, sizeof(local)) < 0) // 如果绑定失败
    {
      lg(Fatal, "bind errno,errno:%d,errstring:%s", errno, strerror(errno));
      exit(BindError);
    }
    lg(Info, "bind socket success,_listsock:%d", _listensock);
    // 3/tcp和udp的区别就是要面向连接 要被动地等待别人来连接
    if (listen(_listensock, backlog) < 0)
    {
      lg(Fatal, "listen errno,errno:%d,errstring:%s", errno, strerror(errno));
    }
    lg(Info, "listen socket success,_listsock:%d", _listensock);
  }

1.1.2 Run-运行服务器(单进程版)

1、接收客户端的连接请求 accept

三次握手完成后, 服务器调用accept()接受连接;

如果服务器调用accept()时还没有客户端的连接请求,就阻塞等待直到有客户端连接上来;

addr是一个传出参数,accept()返回时传出客户端的地址和端口号;

如果给addr 参数传NULL,表示不关心客户端的地址;

addrlen参数是一个传入传出参数(value-result argument), 传入的是调用者提供的, 缓冲区addr的长度 以避免缓冲区溢出问题, 传出的是客户端地址结构体的实际长度(有可能没有占满调用者提供的缓冲区);

我们的服务器程序结构是这样的:

accept的返回值是什么意义??

我们会发现accept返回的也是一个文件描述符,那么这个文件描述符跟我们刚开始的那个listensock是什么关系呢??

举例子:假如我们在假期去一个旅游圣地,到中午的时候很多当地的餐馆为了生意都会安排人站在外头去拉客,比方说你们一行人走到一家鱼庄面前,这时候鱼庄门口有个张三马上靠过来开始给你介绍这里餐馆的特色,邀请你去鱼庄吃饭,这个时候你们正好也饿了于是就进去了 ,但是你们进去的时候张三并没有进去,而是喊了李四这个服务员过来给你们服务,自己又继续出去拉客了,后来张三又不断拉来新的客人,又不断有王五,赵六......服务员也出来了,所以张三只关注于拉客,而李四等服务员只提供服务,他们各自专注着自己的事情却可以把整个鱼庄经营得非常好!!!

所以我们之前的listensock就是我们的张三,他就是专门负责和客户端建立连接的,而accept返回值的sockfd是就相当于是我们的李四,是专门给客户端提供服务的!!

2、开始给客户端提供服务

我们用telnet模拟客户端就可以进行测试了!!

但是这样有一个很尴尬的地方就是,我们当前这个单进程版的必须等到这个服务结束了才会去进行下一个客户端的连接,这样显然是不符合我们的要求的!(有点像客流量很多 但是餐馆只有一张桌子 这样效率很低!!)!所以我们接下来要尝试 多进程版、多线程版、线程池版

1.1.3 Run-运行服务器(多进程版)

多进程思路:让子进程替我去完成工作,而我继续去响应链接!!

(1)父进程把文件描述符表拷贝给子进程后,父进程就要把sockfd给关了(让服务完全由子进程去做,如果子进程退出了意味着服务结束,这样正好可以把这个文件给关了) 而子进程可以把listensockfd给关了(让链接完全由父进程去做,防止子进程误操作)

(2)但是如果父进程阻塞等待的话,又会和单进程一样,而如果用非阻塞轮询又会浪费cpu资源,且增加程序设计的复杂性 所以我们要思考其他办法!!

方法1:让孙子进程去做 然后子进程退出 这样的话父进程立马可以返回 而孙子进程会被系统领养 由系统回收

方法2:直接将SIGCHLD信号设成SIG_IGN 那么系统就不会把退出的进程转成僵尸进程

1.1.4 Run-运行服务器 (多线程版)

但是多进程太耗费资源了!!所以我们应该考虑多线程版!!

(1)多线程不需要关闭文件描述符 因为是共享的所以没有多余的

(2)如果join的话又会阻塞住,所以我们可以直接将线程给分离了,这样主线程就不关心了!

(3)定义一个类将属性传给线程

如果线程调用的函数写在里面,默认有this指针,所以必须把他设置成静态成员函数!!

但是静态成员函数不能调用非静态成员函数,所以我们可以把对象指针传进去,通过这个对象指针来调用成员函数。

1.1.5 Run-运行服务器(线程池版汉英翻译)

我不希望你客户端连接成功后我才去创建一个线程,而一个客户断开了又得释放进程,这样效率太低了!!而且我不想给你提供这种长服务(就是你当前客户端如果请求太多的话,我就得一直专门服务你,这就是长服务),而多线程并不适合长服务,因为线程的个数是确定的,不能让你一直给一个客户端服务,所以我们要尝试把他修改成短服务(就是这个客户端一旦接受了你的一次请求他就断掉 继续去服务别的客户端)!!

所以我们(1)一方面需要通过线程池来避免线程被重复创建和释放的过程,(2)另一方面把长服务设置成短服务!(3)然后将具体任务封装起来交给线程池去完成,这样可以解耦

我们可以将这个任务设置成英汉翻译

dict.txt

apple:苹果...

banana:香蕉...

red:红色...

yellow:黄色...

the: 这

be: 是

to: 朝向/给/对

and: 和

I: 我

in: 在...里

that: 那个

have: 有

will: 将

for: 为了

but: 但是

as: 像...一样

what: 什么

so: 因此

he: 他

her: 她

his: 他的

they: 他们

we: 我们

their: 他们的

his: 它的

with: 和...一起

she: 她

he: 他(宾格)

it: 它

Init.hpp(读取一个文件 然后分割到哈希表中)

cpp 复制代码
#pragma once

#include <iostream>
#include <string>
#include <fstream>
#include <unordered_map>
#include "Log.hpp"

const std::string dictname = "./dict.txt";
const std::string sep = ":";

//yellow:黄色...
static bool Split(std::string &s, std::string *part1, std::string *part2)
{
    auto pos = s.find(sep);
    if(pos == std::string::npos) return false;
    *part1 = s.substr(0, pos);
    *part2 = s.substr(pos+1);
    return true;
}

class Init
{
public:
    Init()
    {
        std::ifstream in(dictname);
        if(!in.is_open())
        {
            lg(Fatal, "ifstream open %s error", dictname.c_str());
            exit(1);
        }
        std::string line;
        while(std::getline(in, line))
        {
            std::string part1, part2;
            Split(line, &part1, &part2);
            dict.insert({part1, part2});
        }
        in.close();
    }
    std::string translation(const std::string &key)
    {
        auto iter = dict.find(key);
        if(iter == dict.end()) return "Unknow";
        else return iter->second;
    }
private:
    std::unordered_map<std::string, std::string> dict;
};

Task.hpp 任务

cpp 复制代码
#pragma once
#include <iostream>
#include <string>
#include "Log.hpp"
#include "Init.hpp"

extern Log lg;
Init init;

class Task
{
public:
    Task(int sockfd, const std::string &clientip, const uint16_t &clientport)
        : sockfd_(sockfd), clientip_(clientip), clientport_(clientport)
    {
    }
    Task()
    {
    }
    void run()
    {
        // 测试代码
        char buffer[4096];
        // Tcp是面向字节流的,你怎么保证,你读取上来的数据,是"一个" "完整" 的报文呢?
        ssize_t n = read(sockfd_, buffer, sizeof(buffer)); // BUG?
        if (n > 0)
        {
            buffer[n] = 0;
            std::cout << "client key# " << buffer << std::endl;
            std::string echo_string = init.translation(buffer);

            // sleep(5);
            // // close(sockfd_);
            // lg(Warning, "close sockfd %d done", sockfd_);

            // sleep(2);
            n = write(sockfd_, echo_string.c_str(), echo_string.size()); // 100 fd 不存在
            if(n < 0)
            {
                lg(Warning, "write error, errno : %d, errstring: %s", errno, strerror(errno));
            }
        }
        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_);
        }
        close(sockfd_);
    }
    void operator()()
    {
        run();
    }
    ~Task()
    {
    }

private:
    int sockfd_;
    std::string clientip_;
    uint16_t clientport_;
};

线程池ThreadPool:

cpp 复制代码
#pragma once

#include <iostream>
#include <vector>
#include <string>
#include <queue>
#include <pthread.h>
#include <unistd.h>

struct ThreadInfo
{
    pthread_t tid;
    std::string name;
};

static const int defalutnum = 10;

template <class T>
class ThreadPool
{
public:
    void Lock()
    {
        pthread_mutex_lock(&mutex_);
    }
    void Unlock()
    {
        pthread_mutex_unlock(&mutex_);
    }
    void Wakeup()
    {
        pthread_cond_signal(&cond_);
    }
    void ThreadSleep()
    {
        pthread_cond_wait(&cond_, &mutex_);
    }
    bool IsQueueEmpty()
    {
        return tasks_.empty();
    }
    std::string GetThreadName(pthread_t tid)
    {
        for (const auto &ti : threads_)
        {
            if (ti.tid == tid)
                return ti.name;
        }
        return "None";
    }

public:
    static void *HandlerTask(void *args)
    {
        ThreadPool<T> *tp = static_cast<ThreadPool<T> *>(args);
        std::string name = tp->GetThreadName(pthread_self());
        while (true)
        {
            tp->Lock();

            while (tp->IsQueueEmpty())
            {
                tp->ThreadSleep();
            }
            T t = tp->Pop();
            tp->Unlock();

            t();
        }
    }
    void Start()
    {
        int num = threads_.size();
        for (int i = 0; i < num; i++)
        {
            threads_[i].name = "thread-" + std::to_string(i + 1);
            pthread_create(&(threads_[i].tid), nullptr, HandlerTask, this);
        }
    }
    T Pop()
    {
        T t = tasks_.front();
        tasks_.pop();
        return t;
    }
    void Push(const T &t)
    {
        Lock();
        tasks_.push(t);
        Wakeup();
        Unlock();
    }
    static ThreadPool<T> *GetInstance()
    {
        if (nullptr == tp_) // ???
        {
            pthread_mutex_lock(&lock_);
            if (nullptr == tp_)
            {
                std::cout << "log: singleton create done first!" << std::endl;
                tp_ = new ThreadPool<T>();
            }
            pthread_mutex_unlock(&lock_);
        }

        return tp_;
    }

private:
    ThreadPool(int num = defalutnum) : threads_(num)
    {
        pthread_mutex_init(&mutex_, nullptr);
        pthread_cond_init(&cond_, nullptr);
    }
    ~ThreadPool()
    {
        pthread_mutex_destroy(&mutex_);
        pthread_cond_destroy(&cond_);
    }
    ThreadPool(const ThreadPool<T> &) = delete;
    const ThreadPool<T> &operator=(const ThreadPool<T> &) = delete; // a=b=c
private:
    std::vector<ThreadInfo> threads_;
    std::queue<T> tasks_;

    pthread_mutex_t mutex_;
    pthread_cond_t cond_;

    static ThreadPool<T> *tp_;
    static pthread_mutex_t lock_;
};

template <class T>
ThreadPool<T> *ThreadPool<T>::tp_ = nullptr;

template <class T>
pthread_mutex_t ThreadPool<T>::lock_ = PTHREAD_MUTEX_INITIALIZER;

1.1.6 服务端写入时客户端退出了怎么办

对一个对端已经关闭的socket调用两次write, 第二次将会生成SIGPIPE信号, 该信号默认结束进程.

SIGPIPE信号详解 - 冷冰若水 - 博客园

1.1.7 服务端全部代码

cpp 复制代码
#include <iostream>
#include <string>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h> //套接字类型的头文件
#include <strings.h>    //bzero的头文件
#include <cstring>
#include <arpa/inet.h>
#include "Log.hpp"
#include <functional>
#include <sys/wait.h>
#include <signal.h>
#include <pthread.h>
#include"ThreadPool.hpp"
#include "Task.hpp"
using namespace std;

typedef function<string(const string &)> func_t;
Log lg; // 命令对象 用来打印日志信息
enum
{
  UsageError = 1, // 使用有误
  SocketError,    // 创建套接字有误
  BindError,      // 绑定有误
  ListenError,    // 监听有误
};

const int defaultfd = -1;
const uint16_t defaultport = 8080;
const string defaultip = "0.0.0.0";
const int size = 1024;
const int backlog = 10; // 但是一般不要设置的太大

class ThreadData
{
public:
  ThreadData(int fd, uint16_t &port, const string &ip, TcpServer *t) : sockfd(fd), clientport(port),clientip(ip), tsvr(t)
  {
  }
  public:
  int sockfd;
  string clientip;
  uint16_t clientport;
  TcpServer *tsvr;//通过对象让静态成员函数调用 非静态成员方法
};

class TcpServer
{
public:
  TcpServer(uint16_t &port, const string &ip = defaultip) : _listensock(defaultfd), _port(port), _ip(ip), _isrunning(false)
  {
  }

  static void *Routine(void *args)
  {
    pthread_detach(pthread_self());
    ThreadData *td = static_cast<ThreadData *>(args);
    td->tsvr->Service(td->sockfd, td->clientport, td->clientip); //通过传一个对象指针来调用类内的成员函数
    delete td;
    return nullptr;
  }

  void Service(int sockfd, uint16_t clientport, string clientip)
  {
    char buffer[size];
    while (true)
    {
      // 服务端要先接收客户端的数据
      ssize_t n = read(sockfd, buffer, sizeof(buffer)); // 读到缓冲区中
      if (n > 0)                                        // 大于0表示读取成功 =0表示当前没有数据可读了  <0说明读取失败
      {
        buffer[n] = 0; // 把我们读到的信息当成是字符串的形式来处理
        cout << "client say#" << buffer << endl;
        string echo_string = "tcpserver echo#";
        echo_string += buffer;
        // 对buffer简单加工完之后往客户端写入
        write(sockfd, echo_string.c_str(), echo_string.size());
      }
      else if (n == 0) // 没有什么数据可读的了 所以八成是客户端链接断开了 我服务端不能崩
      {
        lg(Info, "client quit, sockfd: %d, client ip: %s, client port: %d", sockfd, clientip.c_str(), clientport);
        break;
      }
      else // 读取失败  可能是文件描述符被关闭了
      {
        lg(Warning, "read error, sockfd: %d, client ip: %s, client port: %d", sockfd, clientip.c_str(), clientport);
      }
    }
  }

  void InitServer() // 创建服务器
  {
    // 1/创建套接字
    _listensock = socket(AF_INET, SOCK_STREAM, 0); // 面向字节流;
    if (_listensock < 0)
    {
      lg(Fatal, "create socket,errno:%d,errstring:%s", errno, strerror(errno));
      exit(SocketError);
    }
    lg(Info, "create socket success,_listsock:%d", _listensock);
    // 2/开始绑定
    struct sockaddr_in local;
    bzero(&local, sizeof(local)); // 先清空,然后再填进去
    local.sin_family = AF_INET;
    local.sin_port = htons(_port); // 转网络序列
    inet_aton(_ip.c_str(), &local.sin_addr);
    // 开始绑定
    if (bind(_listensock, (sockaddr *)&local, sizeof(local)) < 0) // 如果绑定失败
    {
      lg(Fatal, "bind errno,errno:%d,errstring:%s", errno, strerror(errno));
      exit(BindError);
    }
    lg(Info, "bind socket success,_listsock:%d", _listensock);
    // 3/tcp和udp的区别就是要面向连接 要被动地等待别人来连接
    if (listen(_listensock, backlog) < 0)
    {
      lg(Fatal, "listen errno,errno:%d,errstring:%s", errno, strerror(errno));
    }
    lg(Info, "listen socket success,_listsock:%d", _listensock);
  }
  ///
  void Run() // 启动服务器
  {
ThreadPool<Task>::GetInstance()->Start();
    signal(SIGPIPE,SIG_IGN);
    _isrunning = true;
    lg(Info, "tcpServer is running....");
    // 1、accept尝试获取新链接
    while (_isrunning) // 不断获取新链接
    {
      struct sockaddr_in client;
      socklen_t len = sizeof(client);
      int sockfd = accept(_listensock, (struct sockaddr *)&client, &len); // 这个id是用来做服务的!!
      if (sockfd < 0)                                                     // 如果获取失败 应该获取下一个 而不是直接结束
      {
        lg(Warning, "accept error, errno: %d, errstring: %s", errno, strerror(errno));
        continue;
      }
      lg(Info, "accept success,sockfd:%d", sockfd);
      // 2、将客户端的信息弄出来
      uint16_t clientport = ntohs(client.sin_port);
      char clientip[32]; // 输出型参数
      inet_ntop(AF_INET, &(client.sin_addr), clientip, sizeof(clientip));
      // 根据新连接来进行通信服务-version单进程版
      // Service(sockfd, clientport, clientip);
      // close(sockfd);
      // version2 多进程版-让子进程帮我做服务,而我继续去链接
      // pid_t id=fork();
      // if(id==0) //child 让他去服务
      // {
      //    close(_listensock);//关掉 防止误操作
      //    if(fork()>0) exit(0);//子进程退掉 让孙子进程来做
      //    Service(sockfd, clientport, clientip);//孙子进程此时已经被 system领养了
      //    close(sockfd);
      //    exit(0);
      // }
      // //father
      // pid_t rid=waitpid(id,nullptr,0);//子进程一进去就退出了,所以父进程会马上返回继续去链接
      // (void)rid;//rid没用过 所以用一下防止警告
      // //signal(SIGCHLD,SIG_IGN);
      // version3 多线程版!!
      // ThreadData *td = new ThreadData(sockfd, clientport, clientip, this);
      // pthread_t tid;
      // pthread_create(&tid, nullptr, Routine, td);
      //version4 线程池英汉词典
       Task t(sockfd, clientip, clientport);
       ThreadPool<Task>::GetInstance()->Push(t);
    }
  }
  ~TcpServer()
  {
  }

private:
  int _listensock; // 监听的文件描述符
  string _ip;      // 服务端ip
  uint16_t _port;  // 端口号
  bool _isrunning; // 服务器是否在运行
};

1.2 客户端

客户端帮我们发送connect请求的时候,会自动bind

客户端需要调用connect()连接服务器;

connect和bind的参数形式一致, 区别在于bind的参数是自己的地址, 而connect的参数是对方的地址;

connect()成功返回0,出错返回-1;

单进程版客户端:

cpp 复制代码
#include <iostream>
#include <cstdlib>
#include <unistd.h>
#include <strings.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;
}

// ./tcpclient serverip serverport
int main(int argc,char* argv[]) //必须知道服务器的ip和端口号
{
    if(argc!=3)
    {
      Usage(argv[0]);
      exit(0);
    }
    string serverip = argv[1];
    uint16_t serverport = std::stoi(argv[2]);

    //1、第一步 创建套接字
    int sockfd=socket(AF_INET, SOCK_STREAM, 0);
    if(sockfd<0)
    {
        cerr << "socker error" << endl;
        return 1;
    }
    //2/OS帮助们bind
    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());//字符串转四字节
    inet_pton(AF_INET,serverip.c_str(), &(server.sin_addr));
    socklen_t len = sizeof(server);
    //3、向服务端发送链接请求 
    int n=connect(sockfd,(struct sockaddr*)&server,len);
    if(n<0)//如果链接失败
    {
      cerr<<"connect error......" <<endl;
      return 2;
    }
    //链接成功
    cout<<"connect sucess"<<endl;
    string message; //用来
    char inbuffer[1024];//接收读取的缓冲区
    while(true)
    {
        cout<<"please enter@";
        getline(cin,message); //将获取的信息放到message中 发到服务端
       // 1. 数据 2. 给谁发 
        ssize_t n=write(sockfd,message.c_str(),message.size());//可以直接通过write写到文件里
        //一般不会写失败,因为服务器一般都不关
        //从文件里读
        ssize_t s = read(sockfd,inbuffer,sizeof(inbuffer));//读到我们的缓冲区里
        //会将结果带回来
        if(s > 0)
        {
            inbuffer[s] = 0;
            cout << inbuffer << endl;
        }
    } 
    close(sockfd);
}

线程池版英汉翻译客户端:

(1)需要改成短服务,所以链接在请求一次后就得断掉,所以while循环必须写在链接的前面

(2)我们平时掉线了 就是服务端和客户端断开了,这个时候我们客户端要继续尝试跟服务端建立连接,当然也要限制连接次数 所以可以用一个do while循环放在链接那里

因为每处理一次请求就要断掉,所以while循环必须写到链接前面

cpp 复制代码
#include <iostream>
#include <cstring>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>

using namespace std;

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

// ./tcpclient serverip serverport
int main(int argc, char *argv[]) // 必须知道服务器的ip和端口号
{
    if (argc != 3)
    {
        Usage(argv[0]);
        exit(0);
    }
    string serverip = argv[1];
    uint16_t serverport = std::stoi(argv[2]);

    // 2/OS帮助们bind
    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());//字符串转四字节
    inet_pton(AF_INET, serverip.c_str(), &(server.sin_addr));
    // 3、向服务端发送链接请求
    while (true)
    {
        int cnt = 5;             // 重连次数
        int isreconnect = false; // 是否要尝试重连
        // 创建套接字
        int sockfd =0; 
        sockfd=socket(AF_INET, SOCK_STREAM, 0);
        if (sockfd < 0)
        {
            cerr << "socker error" << endl;
            return 1;
        }
        do
        {
            int n = connect(sockfd, (struct sockaddr *)&server, sizeof(server));
            if (n < 0) // 如果链接失败
            {
                isreconnect = true;
                --cnt;
                cerr << "connect error......" << endl;
            }
            else
                break; // 链接成功就跳出去
        } while (cnt && isreconnect);
        if (cnt == 0)
        {
            cerr << "user ofline" << endl;
            break;
        }
        // 链接成功
        cout << "connect sucess" << endl;
        string message;      // 用来
        char inbuffer[1024]; // 接收读取的缓冲区
        cout << "please enter@";
        getline(cin, message); // 将获取的信息放到message中 发到服务端
        // 1. 数据 2. 给谁发
        ssize_t n = write(sockfd, message.c_str(), message.size()); // 可以直接通过write写到文件里
        if (n < 0)
        {
            std::cerr << "write error..." << std::endl;
            // break; 短服务不用出去
        }
        // 一般不会写失败,因为服务器一般都不关
        // 从文件里读
        n = read(sockfd, inbuffer, sizeof(inbuffer)); // 读到我们的缓冲区里
        // 会将结果带回来
        if (n > 0)
        {
            inbuffer[n] = 0;
            cout << inbuffer << endl;
        }
    close(sockfd);
}
     return 0;
}

当然如果执行的是长服务的话,一旦写入失败就要break出去重连!!

二、守护进程

服务端在我们ctrl c或者关掉xshell的时候就会被杀死,但是我们希望无论如何这个服务端是一直在跑的!!所以我们必须守护进程!!

2.1 Session和前后台进程

每当一个用户登录的就是 默认就会形成一个session,然后分配一个bash进程

前台进程后后台进程的关键在于谁拥有键盘文件!

1、执行可执行程序的时候在后面加个& 该进程就会变成后台进程

2、通过jobs命令可以看到所有后台任务

3、该序号叫做后台进程任务号,我们可以使用fg+序号将后台进程提到前台

4、如果我们将一个后台进程提到前台之后后悔了,我们可以ctrl+z向前台进程发送19号信号,此时当前台进程被暂停时,bash进程就会自动移到前台进程(因为在命令行中,前台必须存在),而暂停的进程自动放到后台。 然后通过bg+序号将因为暂停被放在后台的进程恢复运行!

2.2 进程间关系

1、PGID叫进程组ID,一个组的是一样的 ,只启动一个进程的话就自成一组

2、sessionid用的就是bash进程的pid,而多个进程组在同一个session里面sid是一样的

3、如果我们关掉OS,那么后台进程会收到用户登录和退出的影响 因此我们需要守护进程化

2.3 如何做到

要尝试自成一个会话!!

setsid 自成会话 不能是组长,所以我们必须fork出子进程,然后退出父进程,让子进程执行后面的代码,所以 守护进程的本质也是孤儿进程!

cpp 复制代码
#pragma once

#include <iostream>
#include <cstdlib>
#include <unistd.h>
#include <signal.h>
#include <string>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

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

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

    // 2. 将自己变成独立的会话
    if (fork() > 0)
        exit(0);
    setsid();

    // 3. 更改当前调用进程的工作目录
    if (!cwd.empty())
        chdir(cwd.c_str());

    // 4. 标准输入,标准输出,标准错误重定向至/dev/null
    int fd = open(nullfile.c_str(), O_RDWR);
    if(fd > 0)
    {
        dup2(fd, 0);
        dup2(fd, 1);
        dup2(fd, 2);
        close(fd);
    }
}

(1)忽略其他异常信号

(2)用setsid将自己变成独立会话

(3)更改当前的工作目录

(4)标准输出、输入、错误重定向到/dev/null (守护进程必须和他们解关联,如果我们不往显示器而是文件写入的话还好,但如果我们直接关闭描述符的话显然会导致printf和cout出错!!而/dev/null就是相当于是一个垃圾桶文件,可以把不关心的内容丢到里面去)

所以我们将上述代码在Run函数中运行 然后同时把我们的日志改成写到文件中!!

如果我们想杀掉的话就得用kill -9 PID

2.4 为什么我们能远程登录Linux呢?

其实ssh就是守护进程,我们向他发送链接请求,认证后再登录,然后分配一个会话,然后将命令再远端执行完后再返回给你

一般来说守护进程我们一般在他的名字后面加一个-D

三、TCP协议的通讯流程

3.1 TCP的三次握手和四次挥手

3.2 TCP通信全双工

TCP是全双工的 ,因为发送和接受缓冲区是分开的,多线程时虽然不能多人读,但是支持同时读写!!

3.3 如何理解链接

对于服务器来说,同时存在大量连接,那么谁来打开、谁来关闭、连接状态是什么,所以OS必须要先描述再组织来管理链接

相关推荐
鸡鸭扣40 分钟前
Docker:3、在VSCode上安装并运行python程序或JavaScript程序
运维·vscode·python·docker·容器·js
A ?Charis1 小时前
k8s-对接NFS存储
linux·服务器·kubernetes
饮长安千年月2 小时前
Linksys WRT54G路由器溢出漏洞分析–运行环境修复
网络·物联网·学习·安全·机器学习
是小崔啊3 小时前
java网络编程02 - HTTP、HTTPS详解
java·网络·http
人工干智能4 小时前
科普:“Docker Desktop”和“Docker”以及“WSL”
运维·docker·容器
落笔画忧愁e4 小时前
FastGPT及大模型API(Docker)私有化部署指南
运维·docker·容器
前端郭德纲4 小时前
前端自动化部署的极简方案
运维·前端·自动化
车载诊断技术5 小时前
电子电气架构 --- 电子电器新技术及发展趋势
网络·架构·汽车·电子电器框架·车载充电器(obc)·电子电器新技术及发展趋势
卷心菜不卷Iris5 小时前
第1章大型互联网公司的基础架构——1.6 RPC服务
网络·网络协议·微服务·rpc·http协议·rpc协议
DC_BLOG5 小时前
Linux-GlusterFS进阶配置
linux·运维·服务器