三.网络编程套接字_TCP

news2025/3/9 22:47:38

一.序言

在上一章中,我们已经实现了用udp来实现网络编程,这一节我们用tcp来实现网络编程,通过对比两者编写过程的区别,来加深对udp,tcp的理解!
(两者其实差别不大!有了udp的基础,学习起来tcp会相对轻松很多)
同样的,用户端可以接收来自服务器端的消息,也可以向服务器端发消息
服务器端可以接收用户端的消息,也可以向用户端发消息
在这里插入图片描述
在编写tcp代码之前,我们首先讲一个小故事
话说我们有时路过一些饭店/服装店,可能会遇到一个揽客的人,会非常"热情好客"地向你推销服务,向你安利饭菜有多好吃(衣服有多精美便宜)等等,吸引你进去商铺里面
假如你感兴趣的话,就会走进店铺里面看看,然后揽客的人会叫店员(服务员)来招待你,然后负责揽客的人,会继续去招揽下一批顾客
假如不感兴趣,你在犹豫几次后,就决定去找一家更好吃的餐厅(更精美的服装店等等),而这并没有什么大不了,揽客的人不会强迫你必须进入商店里面进行消费
tcp对比udp的差别也正是在这,tcp在整个过程中,存在两个套接字
一个我们可以称之为_listensock监听套接字,它就相当于揽客的人
另一个我们可以称之为_sock服务套接字,它就相当于实际为我们提供服务的店员
tcp不是"强买强卖",服务器端和用户端两者需要相互"沟通",也就是我们上节提到的tcp是一个基于连接的流式服务
揽客的人,需要出去外面吆喝揽客(listen),随时准备接收(accept)新顾客,而路过的人,要看是否被打动(connect),走进店里面,成为顾客
更具体的,我们通过编写代码来加深理解!

二.简单tcp服务器端编写

tcp服务器和udp服务器端两者编写过程中,有很多相似之处,或者说整体大框架大差不差,不过会多了几个系统接口需要调用.

2.1服务器server端

2.1.1初始化服务器

1.创建套接字

首先还是调用socket系统接口,创建套接字
不过和udp不同,创建套接字时所需的服务类型应该是SOCK_STREAM(流式服务),而不是SOCK_DGRAM(数据报)
并且,我们此时创建的对应套接字,是监听套接字,而不是服务套接字!还没有到需要提供服务的那一步,现在还在完成准备工作而已.

// 1.创建socket接口,创建对应的网络文件
_listensock = socket(AF_INET, SOCK_STREAM, 0);
// 假如创建失败,
if (_listensock < 0)
{
  std::cerr << "create socket err: " << strerror(errno) << std::endl;
  exit(SOCKET_ERR);
}
std::cout << "create socket success: " << _listensock << std::endl;
2.绑定

同样的,套接字创建完毕后我们实际只是在系统层面上打开了一个文件,该文件还没有与网络关联起来,因此创建完套接字后我们还需要调用bind系统接口进行绑定,与网络关联起来,这和udp,是一样的操作!
同样的,在构建sockaddr_in结构体的时候,我们的ip地址是随意指定的,而不是将服务器server的ip固定赋值
原因,我们在上一节也提到过,一个进程可以关联多个端口号
一台服务器底层可能装有多张网卡,此时这台服务器就可能会有多个IP地址,这台服务器在接收数据时,这里的多张网卡在底层实际都收到了数据,但假如绑定某一个特定的IP地址,那只能从绑定IP对应的网卡接收数据;
但假如随机绑定(INADDR_ANY),那就能从随机一张对应IP的网卡接收数据,可以提高服务器的负载均衡能力,并避免单个网卡成为瓶颈
并且INADDR_ANY本质就是宏定义的0,因此在设置时不需要进行网络字节序的转换

// 2.绑定
struct sockaddr_in local;
bzero(&local, sizeof(local));  // 将结构体清零
local.sin_family = AF_INET;    // 确定协议家族
local.sin_port = htons(_port); // 将端口号转成大端(网络)字节序

// 云服务器,或者一款服务器,一般不要指明某一个确定的IP;本地安装的虚拟机,或者物理机器是允许的
local.sin_addr.s_addr = INADDR_ANY; // 随机指定任意一个ip
if (bind(_listensock, (struct sockaddr *)&local, sizeof(local)))
{
  std::cerr << "bind socket err: " << strerror(errno) << std::endl;
  exit(BIND_ERR);
}
std::cout << "bind socket success: " << _listensock << std::endl;
3.监听listen

udp服务器可没有监听这一步,而tcp需要监听这一步,原因我们也在序言说了,这是它基于连接这一特性所决定的!

tcp服务器需要时刻注意是否有客户端发来连接请求,此时就需要将tcp服务器创建的套接字设置为监听状态
对应我们的小故事,也就是商铺(服务器端),要派出我们的吆喝工作人员,在门店前,大街上到处乱逛,招揽顾客,与顾客(用户端)进行"连接"
设置套接字为监听状态的系统接口叫做listen,该函数的函数原型如下:

int listen(int sockfd, int backlog);

在这里插入图片描述
在这里插入图片描述

总共有两个参数
第一个参数就是我们刚刚创建好的监听套接字,或者说网络文件描述符
sockfd:需要设置为监听状态的套接字对应的文件描述符
第二个参数是全队列的最大长度.
一般设置为2的倍数,16,32,64等等,一般不要设置的太大.
在这里插入图片描述
返回值也是老套路,成功返回0,不成功,返回-1,并设置对应的错误码
在这里插入图片描述
具体代码如下:

// 3.监听
if (listen(_listensock, backlog) < 0)
{
  std::cerr << "listen socket err: " << strerror(errno) << std::endl;
  exit(LISTEN_ERR);
}
std::cout << "listen socket success: " << _listensock << std::endl;
4.整体代码展示
void InitServer()
{
  // 1.创建socket接口,创建对应的网络文件
  _listensock = socket(AF_INET, SOCK_STREAM, 0);
  // 假如创建失败,
  if (_listensock < 0)
  {
    std::cerr << "create socket err: " << strerror(errno) << std::endl;
    exit(SOCKET_ERR);
  }
  std::cout << "create socket success: " << _listensock << std::endl;

  // 2.绑定
  struct sockaddr_in local;
  bzero(&local, sizeof(local));  // 将结构体清零
  local.sin_family = AF_INET;    // 确定协议家族
  local.sin_port = htons(_port); // 将端口号转成大端字节序

  // 云服务器,或者一款服务器,一般不要指明某一个确定的IP;本地安装的虚拟机,或者物理机器是允许的
  local.sin_addr.s_addr = INADDR_ANY; // 随机指定任意一个ip
  if (bind(_listensock, (struct sockaddr *)&local, sizeof(local)))
  {
    std::cerr << "bind socket err: " << strerror(errno) << std::endl;
    exit(BIND_ERR);
  }
  std::cout << "bind socket success: " << _listensock << std::endl;

  // 3.监听
  if (listen(_listensock, backlog) < 0)
  {
    std::cerr << "listen socket err: " << strerror(errno) << std::endl;
    exit(LISTEN_ERR);
  }
  std::cout << "listen socket success: " << _listensock << std::endl;
}

2.1.2实现start函数

1.获取连接accept

tcp服务器初始化后就可以开始运行了,但tcp服务器在与客户端进行网络通信之前,服务器还需要先获取到客户端的连接请求
对应到我们的小故事中,就是吆喝的工作人员,在那吆喝(设置为了监听模式),但是这并不代表着路过的人就会被吸引进入店铺!
路过对商铺感兴趣的人向吆喝的工作人员发出有入店的意愿,吆喝的人员接收到了,路过的人才会真正成为顾客!这是一个双向选择的过程.
获取连接的函数叫做accept,该函数的函数原型如下:

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

在这里插入图片描述
该系统接口总共有三个参数
第一个参数是socket,也就是传入我们设置为监听状态的套接字对应的文件描述符,表示从该监听套接字中获取连接
在这里插入图片描述
第二个参数是address,传入的是用户端结构体sockaddr_in的地址
在这里插入图片描述
第三个参数是address_len,传入的是用户端结构体sockaddr_in的大小
在这里插入图片描述
和我们在udp编程中recvfrom函数参数非常类似(当然参数个数不同),后两者其实都是输入输出型参数,成功获取连接后,结构体sockaddr_in会成功填入对应连接的用户端的ip,port等等信息
在逻辑上也很好理解?
成功连接后,客户端也必须获取对应用户端ip,port等等信息
这样才能有后续的服务(发消息等等),不然网络中两个唯一的进程如何进行交流呢?
不管连接是否成功,其实影响不大,并不会需要报错,终止程序
揽客的人不一定每一次出击都能招揽到客人
假如连接失败,就不断循环,重新尝试和用户端进行连接
返回参数是我们的服务套接字,监听套接字的任务只是不断获取新连接,而真正为这些连接提供服务的套接字是现在返回的参数套接字(服务套接字)

在这里插入图片描述
在我们故事中,对应的就是揽客的人和实际为顾客提供的人,并不是同一个员工,两者具有不同的任务.

struct sockaddr_in client;
socklen_t len = sizeof(client);
// 4.获取连接
int sock = accept(_listensock, (struct sockaddr *)&client, &len);
if (sock < 0)
{
  std::cerr << "accept socket err: " << std::endl;
  continue;  //并不会有任何报错,只是会不断循环,尝试与用户端进行连接
}
//提取client信息
std::string clientip = inet_ntoa(client.sin_addr);  //将网络ip转成点分十进制字符串
uint16_t clientport = ntohs(client.sin_port);  //网络字节序转成小端
printf("获取新连接成功: %d from %d, who: %s - %d",sock,_listensock,clientip.c_str(),clientport);

2.收/发数据(接收用户端发来的数据/向用户端发数据)

在udp服务器时,我们调用recvfrom系统接口,sendto系统接口,来对网络文件进行读写操作
但tcp套接字,我们提到过,它还有一个特点是流式服务
我们提到过流这个概念,是在对普通文件进行读写操作的时候
tcp套接字创建的"网络文件"也不例外,对它进行读写操作的系统接口函数,直接就是我们熟悉的read,write系统接口函数
read系统接口函数

ssize_t read(int fd, void *buf, size_t count);

在这里插入图片描述
第一个参数就是我们要读取的文件所对应的文件描述符,传入的是我们获取到的服务套接字
第二个,第三个参数就是我们的缓冲区地址以及对应的大小,也很好理解,读出来的数据总应该找一个地方存起来吧!
需要值得注意的是返回参数,调用该函数后,会返回成功读取到的字节数
如果返回值大于0,则表示本次实际读取到的字节个数
(假如读取到的字节数比我们预期要小,是正常的,在手册中,列举了几种可能会出现这样的情况,比如在读取时,被发信号终止等等,此时会返回-1,并设置对应的错误码)
如果返回值等于0,则表示对端已经把连接关闭了。
如果返回值小于0,则表示读取时遇到了错误
在这里插入图片描述
wrtie接口函数

ssize_t write(int fd, const void *buf, size_t count);

在这里插入图片描述
第一个参数就是我们要读取的文件所对应的文件描述符,传入的是我们获取到的服务套接字
第二个,第三个参数就是我们的缓冲区地址以及对应的大小,也很好理解,我们要往文件里面写数据,那要写的数据是什么呢?也需要一个缓冲区存起来吧!
返回的参数则是成功写入的字节数,假如为0,则什么都没写入;发生错误的话,-1被返回,错误码errno被设置.
在这里插入图片描述
将对网络文件读写的操作封装为一个类方法,则代码如下:

void service(int sock,const std::string &clientip,const uint16_t &clientport)
{
    std::string who = clientip + ":" + std::to_string(clientport);
    char buffer[1024];  //创建一个缓冲区,用来存储数据
    while(true)
    {
      ssize_t n = read(sock,buffer,sizeof(buffer) - 1); //最后一个字符在C中默认为/0,预留一个位置
      if(n > 0)  //假如成功读取到数据
      {
          buffer[n] = 0;//缓冲区最后一个字符,填充/0
          std::string message = _func(buffer); //对收到的数据进行处理
          std::cout << who << " >> " << message << std::endl;  //说明是谁发出的数据
          write(sock,message.c_str(),message.size());  //往里面写数据
      }
      else if(n == 0)
      {
        // 对方将连接关闭了
        std::cout << who << " quit, me too" << std::endl;
        break;
      }
      else{
        std::cerr << "read error: " << strerror(errno) << std::endl;
        break;
      }
    }
}

其中,_func是我们的类内成员,相当于c中的函数指针

using func_t = std::function<std::string(const std::string &)>;

在这里插入图片描述
通过这样的方式,则只需要在上层编写相应的服务代码,初始化构造server对象时,直接传入即可,完美将服务内容与提供服务的服务器进行解耦.

2.1.3 整体代码展示

//server.hpp
#include <iostream>
#include <cstring>
#include <cstdlib>
#include <unistd.h>
#include <functional>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "err.hpp"

namespace ns_server
{
  const static uint16_t default_port = 8080;
  static const int backlog = 32;

  using func_t = std::function<std::string(const std::string &)>;
  class tcp_Server
  {
  public:
    tcp_Server(func_t func,uint16_t port = default_port)
        : _func(func),_port(port), _quit(true)
    {
      std::cout << "server addrs: " << _port << std::endl;
    }
    void InitServer()
    {
      // 1.创建socket接口,创建对应的网络文件
      _listensock = socket(AF_INET, SOCK_STREAM, 0);
      // 假如创建失败,
      if (_listensock < 0)
      {
        std::cerr << "create socket err: " << strerror(errno) << std::endl;
        exit(SOCKET_ERR);
      }
      std::cout << "create socket success: " << _listensock << std::endl;

      // 2.绑定
      struct sockaddr_in local;
      bzero(&local, sizeof(local));  // 将结构体清零
      local.sin_family = AF_INET;    // 确定协议家族
      local.sin_port = htons(_port); // 将端口号转成大端字节序

      // 云服务器,或者一款服务器,一般不要指明某一个确定的IP;本地安装的虚拟机,或者物理机器是允许的
      local.sin_addr.s_addr = INADDR_ANY; // 随机指定任意一个ip
      if (bind(_listensock, (struct sockaddr *)&local, sizeof(local)))
      {
        std::cerr << "bind socket err: " << strerror(errno) << std::endl;
        exit(BIND_ERR);
      }
      std::cout << "bind socket success: " << _listensock << std::endl;

      // 3.监听
      if (listen(_listensock, backlog) < 0)
      {
        std::cerr << "listen socket err: " << strerror(errno) << std::endl;
        exit(LISTEN_ERR);
      }
      std::cout << "listen socket success: " << _listensock << std::endl;
    }

    void Start()
    {
      _quit = false; // 不需要退出
      // 服务器一旦启动,就一直死循环的进行下去
      while (!_quit)
      {
        struct sockaddr_in client;
        socklen_t len = sizeof(client);
        // 4.获取连接
        int sock = accept(_listensock, (struct sockaddr *)&client, &len);
        if (sock < 0)
        {
          std::cerr << "accept socket err: " << std::endl;
          continue;  //并不会有任何报错,只是会不断循环,尝试与用户端进行连接
        }
        //提取client信息
        std::string clientip = inet_ntoa(client.sin_addr);  //将网络ip转成点分十进制字符串
        uint16_t clientport = ntohs(client.sin_port);  //网络字节序转成小端
        printf("获取新连接成功: %d from %d, who: %s - %d",sock,_listensock,clientip.c_str(),clientport);

        service(sock,clientip,clientport);
      }
    }

    void service(int sock,const std::string &clientip,const uint16_t &clientport)
    {
       std::string who = clientip + ":" + std::to_string(clientport);
       char buffer[1024];  //创建一个缓冲区,用来存储数据
       while(true)
       {
          ssize_t n = read(sock,buffer,sizeof(buffer) - 1); //最后一个字符在C中默认为/0,预留一个位置
          if(n > 0)  //假如成功读取到数据
          {
             buffer[n] = 0;//缓冲区最后一个字符,填充/0
             std::string message = _func(buffer);
             std::cout << who << " >> " << message << std::endl;  //说明是谁发出的数据
             write(sock,message.c_str(),message.size());  //往里面写数据
          }
          else if(n == 0)
          {
            // 对方将连接关闭了
            std::cout << who << " quit, me too" << std::endl;
            break;
          }
          else{
            std::cerr << "read error: " << strerror(errno) << std::endl;
            break;
          }
       }
    }
    ~tcp_Server()
    {
    }

  private:
    int _listensock;
    uint16_t _port; // 端口号
    bool _quit;     // 是否要运行
    func_t _func;   // 处理信息函数
  };
}
//server.cc
#include "tcp_server.hpp"
#include <memory>
#include <cstdio>
using namespace ns_server;
using namespace std;


//用户使用手册
static void Usage(string proc)
{
   std::cout << "Usage:\n\t" << proc << " serverport\n" << std::endl;
}

//上层的业务处理
std::string echo(std::string request)
{
   return request;
}



// ./udp_server port
int main(int argc,char* argv[])
{   
    //假如传入的参数不是两个,而不是指定了对应的端口号,则打出对应的使用列表,并退出
    if(argc != 2)
    {
        Usage(argv[0]);
        exit(USAGE_ERR);
    }
    uint16_t port = atoi(argv[1]);
    unique_ptr<tcp_Server> tsrv(new tcp_Server(echo,port));
    
    tsrv->InitServer();
    tsrv->Start();
    return 0;
}

2.2 用户client端

client用户端的编写,和server服务器端也是相类似的,仅仅在初始用户端有一点点差别而已.

2.2.1 初始化用户端

用户端不需要进行绑定,也不需要进行监听!
不需要绑定的原因,我们在udp一节已经解释过,简单而言,如果固定绑定某个端口号,就有可能一个端口号重复使用,进而一个进程可以执行,而另一个进程无法执行,所以一般都是OS随机进行分配
不需要监听的原因就更好理解了,顾客并不是揽客的人,服务端需要通过监听来获取新连接,但是用户端并不需要,所以也就不需要监听操作.
初始化用户端,只需要创建套接字(没有监听套接字,服务套接字这一说法)

// 创建套接字
int sock = socket(AF_INET, SOCK_STREAM, 0);
if (sock < 0)
{
    std::cerr << "create socket fail:" << strerror(errno) << std::endl;
    exit(SOCKET_ERR);
}
// 不需要自己bind,也不需要accept,只需要考虑连接服务器即可

2.2.2 运行用户端

1.连接服务器端connect

假如路过的人对吆喝的人所讲的内容感兴趣,则可以询问具体内容,然后进一步决定要不要进去商铺里面.
服务器端不断accept,获取连接;对应用户端则是不断尝试connect,连接服务器端,这也是tcp基于连接特点的体现!
发起连接请求的函数叫做connect,该函数的函数原型如下:

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

在这里插入图片描述
第一个参数就是我们要读取的文件所对应的文件描述符,传入的是我们获创建的套接字
第二个,第三个参数就是我们服务器端的sockaddr_in结构体的地址以及对应的大小
假如成功连接,则返回0;否则返回-1,错误码被设置
在这里插入图片描述
同样的,和udp程序类似
我们在编写服务器端的时候,服务器端要做的,只是需要在accept的时候自己设定一个sockaddr_in结构体,便可以获取对应与之相连的用户端ip和port号,两个进程就可以建立起联系!
但是用户端不同,用户是没有对应服务器端的ip和端口号的
你不能说调用accept系统接口,来获取服务器端的ip和端口号,用户是访问者,而不是被访问者
所以用户端进程获取服务器端的ip和端口号,不是用输入输出型参数的方法,而是我们自己给!
在这里插入图片描述
连接代码如下:

//创建服务器端的套接字
struct sockaddr_in server;
memset(&server, 0, sizeof(server)); // 清空对应结构体的内容
server.sin_family = AF_INET;
server.sin_port = htons(serverport); //存储对应的端口号
inet_aton(serverip.c_str(),&server.sin_addr); //存储对应的ip

int connect_cnt = 5; //连接次数
while(connect(sock,(struct sockaddr*)&server,sizeof(server)) < 0)
{
    sleep(1);
    std::cout << "正在给你尝试重连,重连次数还有: " << connect_cnt-- << std::endl;
    if(connect_cnt < 0) break; 
}
//从循环出来,有两种情况,一是连接成功;二是连接次数用完
if(connect_cnt <= 0) //连接次数用完
{
    std::cerr << "连接失败...,code:" << errno << " code string: " << strerror(errno) << std::endl;
    exit(CONNECT_ERR);
}
std::cout << "connect success" << std::endl;
2.收/发数据(接收服务器端数据/向服务器端发送数据)

和服务器端一样,同样是直接调用read,write系统接口就好,没有什么新的知识需要补充.

char buffer[1024];
//连接成功,则可以收发数据
while (true)
{
    // 用户输入
    std::string message;
    std::cout << "Please input your message# ";
    std::getline(std::cin,message);
    //发数据
    write(sock,message.c_str(),message.size());
    
    //收数据
    ssize_t n = read(sock,buffer,sizeof(buffer));
    if (n > 0)
    {
        buffer[n] = 0;
        std::cout << "server echo>>> " << buffer << std::endl;
    }
    else if(n == 0)
    {
        std::cerr << "server quit" << std::endl;
        break;
    }
    else
    {
        std::cerr << "read error:" << strerror(errno) << std::endl;
        break;
    }
}

//关闭文件
close(sock);

2.2.3 整体代码展示

//client.hpp
#pragma once

#include <iostream>
#include <cstring>
#include <cstdlib>
#include <unistd.h>
#include <sys/types.h>        
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "err.hpp"
//client.cc
#include "tcp_client.hpp"

// 127.0.0.1 本地环回 lo(loop) 进行测试客户端,服务器代码

//./文件.cpp serverip serverport
// 用户使用手册
static void Usage(std::string proc)
{
    std::cout << "Usage:\n\t" << proc << " serverip serverport\n"
              << std::endl;
}

int main(int argc, char *argv[])
{
    if (argc != 3)
    {
        Usage(argv[1]);
        exit(USAGE_ERR);
    }
    // 获取到服务器端的ip和port
    std::string serverip = argv[1];
    u_int16_t serverport = atoi(argv[2]);

    // 创建套接字
    int sock = socket(AF_INET, SOCK_STREAM, 0);
    if (sock < 0)
    {
        std::cerr << "create socket fail:" << strerror(errno) << std::endl;
        exit(SOCKET_ERR);
    }
    // 不需要自己bind,也不需要accept,只需要考虑连接服务器即可
    //创建服务器端的套接字
    struct sockaddr_in server;
    memset(&server, 0, sizeof(server)); // 清空对应结构体的内容
    server.sin_family = AF_INET;
    server.sin_port = htons(serverport); //存储对应的端口号
    inet_aton(serverip.c_str(),&server.sin_addr); //存储对应的ip
    
    int connect_cnt = 5; //连接次数
    while(connect(sock,(struct sockaddr*)&server,sizeof(server)) < 0)
    {
        sleep(1);
        std::cout << "正在给你尝试重连,重连次数还有: " << connect_cnt-- << std::endl;
        if(connect_cnt < 0) break; 
    }
    //从循环出来,有两种情况,一是连接成功;二是连接次数用完
    if(connect_cnt <= 0) //连接次数用完
    {
        std::cerr << "连接失败...,code:" << errno << " code string: " << strerror(errno) << std::endl;
        exit(CONNECT_ERR);
    }
    std::cout << "connect success" << std::endl;
    
    char buffer[1024];
    //连接成功,则可以收发数据
    while (true)
    {
        // 用户输入
        std::string message;
        std::cout << "Please input your message# ";
        std::getline(std::cin,message);
        //发数据
        write(sock,message.c_str(),message.size());
        
        //收数据
        ssize_t n = read(sock,buffer,sizeof(buffer));
        if (n > 0)
        {
            buffer[n] = 0;
            std::cout << "server echo>>> " << buffer << std::endl;
        }
        else if(n == 0)
        {
            std::cerr << "server quit" << std::endl;
            break;
        }
        else
        {
            std::cerr << "read error:" << strerror(errno) << std::endl;
            break;
        }
    }

    //关闭文件
    close(sock);
    return 0;
}

多进程版本改造

但是聪明的你可能已经发现了,上述代码有一个巨大的漏洞!
在这里插入图片描述
while循环不断运行,尝试和用户连接,这并没有问题!
但是连接成功之后呢?
服务器端就为用户端不断提供服务了
在这里插入图片描述
在service函数里面,又是一个新的循环
这就会导致,服务器端和某一个用户端进行连接后,其它用户端都没办法成功连接,获取相应的服务!
这显然不合理,这意味着,我们使用QQ的时候,只有一个用户可以登录,一旦有人登上QQ,其他人都没办法登上QQ,那还沟通什么呢?
当然,假如你使用上述代码,尝试用多个用户端进行连接的时候,是会显示成功连接的,只不过没有对应的服务提供!

原理:
实际在底层OS会为我们维护一个连接队列,服务端没有accept的新连接就会放到这个连接队列当中(连接队列的最大长度就是通过listen函数的第二个参数来指定的)
因此服务端虽然没有获取(accept)其它客户端发来的连接请求,但是依旧是显示连接成功的

于是我们考虑将它修改为多进程版本,父进程不断accept,获取连接;然后子进程提供对应服务
但是父进程不断运行,又怎么回收子进程呢?
一个简单的做法,就是父进程直接捕捉SIGCHID信号,并将该信号的处理动作设置为忽略,此时父进程就只需专心处理自己的工作,不必关心子进程了
这也是最为推荐的一种做法

void Start()
{
  _quit = false; // 不需要退出
  signal(SIGCHLD,SIG_IGN);
  // 服务器一旦启动,就一直死循环的进行下去
  while (!_quit)
  {
    struct sockaddr_in client;
    socklen_t len = sizeof(client);
    // 4.获取连接
    int sock = accept(_listensock, (struct sockaddr *)&client, &len);
    if (sock < 0)
    {
      std::cerr << "accept socket err: " << std::endl;
      continue;  //并不会有任何报错,只是会不断循环,尝试与用户端进行连接
    }
    //提取client信息
    std::string clientip = inet_ntoa(client.sin_addr);  //将网络ip转成点分十进制字符串
    uint16_t clientport = ntohs(client.sin_port);  //网络字节序转成小端
    printf("获取新连接成功: %d from %d, who: %s - %d",sock,_listensock,clientip.c_str(),clientport);

    pid_t id = fork(); //创建进程
    if(id == 0)
    {
        //child process
        close(_listensock);  //关闭监听套接字
        service(sock,clientip,clientport);
        exit(0);  //完成任务后,退出
    }
    
  }
}

还有一种做法,是创建孙子进程
成功创建出子进程后,再立马创建孙子进程,并退出子进程,这样孙子进程就会成为孤儿进程,而被OS系统领养,此时父进程专心accept获取连接即可,孙子进程完成任务后,会被OS回收,父进程不需要关注孙子进程!

void Start()
{
  _quit = false; // 不需要退出
  // 服务器一旦启动,就一直死循环的进行下去
  while (!_quit)
  {
    struct sockaddr_in client;
    socklen_t len = sizeof(client);
    // 4.获取连接
    int sock = accept(_listensock, (struct sockaddr *)&client, &len);
    if (sock < 0)
    {
      std::cerr << "accept socket err: " << std::endl;
      continue;  //并不会有任何报错,只是会不断循环,尝试与用户端进行连接
    }
    //提取client信息
    std::string clientip = inet_ntoa(client.sin_addr);  //将网络ip转成点分十进制字符串
    uint16_t clientport = ntohs(client.sin_port);  //网络字节序转成小端
    printf("获取新连接成功: %d from %d, who: %s - %d",sock,_listensock,clientip.c_str(),clientport);

    pid_t id = fork(); //创建进程
    if(id < 0)
    {
        //创建进程失败
        close(sock);
        continue;
    }
    else if(id == 0)
    {
        //child process
        close(_listensock);  //关闭监听套接字
        if(fork() > 0)  exit(0);  //创建孙子进程来提供服务,同时原来的子进程直接退出
        service(sock,clientip,clientport); //孙子进程进行任务处理
        exit(0);  //完成任务后,退出
    }
    //parent process
    //一定关闭掉不需要的fd, 不关闭,会导致fd泄漏
    close(sock);
    pid_t ret = waitpid(id, nullptr, 0); //回收子进程
    if(ret == id)  std::cout << "wait child " << id << " success" << std::endl;
  }
}

细节:
对于进程来说,什么套接字不再使用,就直接关闭对应的"网络文件"!
不然来一个用户,就占据一个sock服务套接字,那对于父进程来说,可用的套接字数目就会越来越少,直接用完了,那孙子进程也不用干活了,工作套接字都没了,还干什么活(文件描述符表会直接继承)

多线程版本改造

申请一个进程,其实消耗还是蛮大的(创建进程意味着需要创建对应的进程控制块(task_struct)、进程地址空间(mm_struct)、页表等数据结构)
一两个进程看不出什么区别,但假如到了QQ,微信,抖音等等几十亿用户的层次,这个消耗(空间,时间资源)将会非常恐怖
所以将单执行流修改为多执行,除了多进程,还有一种相对消耗小很多的方式——多线程(每个线程共享进程的大部分资源)
创建多线程,无法避免需要讨论的一个问题,就是pthread_create传入的参数问题
在这里插入图片描述
每个线程运行,都要调用service方法,但是service方法,需要client用户的ip,port号等等,不然怎么知道为哪个用户端提供服务呢?(两个进程之间的唯一通信)
因此,args参数传入的必定是一个结构体,包含连接的用户端的ip,port等等
还有什么需要传进去呢?
在这里插入图片描述
看起来结构体里面还需要包含sock服务套接字
还有一点最重要的,不能忘记,这是类内函数!实际还有一个默认参数this对象指针,想要调用该方法,结构体就必须还含有this指针!
所以我们设计结构体如下:

class Thread_Data
{
public:
    Thread_Data(int fd,const std::string &ip,const int16_t &port,tcp_Server *ts)
    :_sock(fd),_clientip(ip),_clientport(port),_ts(ts)
    {}
public:
    int _sock;
    std::string _clientip;
    int16_t _clientport;
    tcp_Server* _ts;
};

然后设计Thread_Routine函数如下:

1.记住,pthread_create函数第三个参数,传入参数只有一个,所以要将它设置为static
2.记住线程分离,一旦调用方法,马上detach,这样就不用考虑父线程回收问题

//线程要执行的方法
static void* ThreadRoutine(void* args)
{
    pthread_detach(pthread_self());  //子线程完成任务后,系统自动回收,父线程不用阻塞等待
    Thread_Data* td = static_cast<Thread_Data*>(args);
    td->_ts->service(td->_sock,td->_clientip,td->_clientport);
    delete td;
    return nullptr;
}

这样我们就可以调用pthread_create函数创建线程,并自己运行了!

pthread_t pid;
Thread_Data* ts = new Thread_Data(sock,clientip,clientport,this);
pthread_create(&pid,nullptr,ThreadRoutine,ts);  //创建子线程,并完成对应的service任务

整体代码如下:

void Start()
    {
      _quit = false; // 不需要退出
      //signal(SIGCHLD,SIG_IGN);
      // 服务器一旦启动,就一直死循环的进行下去
      while (!_quit)
      {
        struct sockaddr_in client;
        socklen_t len = sizeof(client);
        // 4.获取连接
        int sock = accept(_listensock, (struct sockaddr *)&client, &len);
        if (sock < 0)
        {
          std::cerr << "accept socket err: " << std::endl;
          continue;  //并不会有任何报错,只是会不断循环,尝试与用户端进行连接
        }
        //提取client信息
        std::string clientip = inet_ntoa(client.sin_addr);  //将网络ip转成点分十进制字符串
        uint16_t clientport = ntohs(client.sin_port);  //网络字节序转成小端
        printf("获取新连接成功: %d from %d, who: %s - %d",sock,_listensock,clientip.c_str(),clientport);

        pthread_t pid;
        Thread_Data* ts = new Thread_Data(sock,clientip,clientport,this);
        pthread_create(&pid,nullptr,ThreadRoutine,ts);  //创建子线程,并完成对应的service任务
      }
    }
    //线程要执行的方法
    static void* ThreadRoutine(void* args)
    {
        pthread_detach(pthread_self());  //子线程完成任务后,系统自动回收,父线程不用阻塞等待
        Thread_Data* td = static_cast<Thread_Data*>(args);
        td->_ts->service(td->_sock,td->_clientip,td->_clientport);
        delete td;
        return nullptr;
    }

编译时需要携带上-pthread选项

.PHONY:all
all: tcp_client tcp_server

tcp_client:tcp_client.cc
	g++ -o $@ $^ -std=c++11 -lpthread
tcp_server:tcp_server.cc
	g++ -o $@ $^ -std=c++11 -lpthread
.PHONY:clean
clean:
	rm -f tcp_client tcp_server

线程池改造

但是,我们说线程创建消耗比进程小,并不是没有消耗![1]
如果有大量的客户端连接请求,此时服务端要为每一个客户端创建对应的服务线程。线程越多,CPU的压力就越大,因为CPU要不断在这些线程之间来回切换,此时CPU在调度线程的时候,线程和线程之间切换的成本就会变得很高.[2]
此时,引入我们之前介绍过的线程池,预先创建一批线程为我们服务(解决问题【1】),并且服务完后,并不直接销毁线程,而是继续完成提供下一个服务,假如没有下一个任务,则进入休眠状态,等待被唤醒(解决问题【2】)
线程池代码如下:(具体参照以前编写的线程池代码)
设计为单例模式,默认线程个数我们设置为20个
也就是一旦有用户连入服务器端,立马就会创建出20个线程提供服务

#pragma once
#include <iostream>
#include <vector>
#include <queue>
#include "Task.hpp"
#include <pthread.h>
#include "Thread.hpp"
#include "mymutex.hpp"

const static int N = 20;  //默认线程数量
template <class T>
class ThreadPool
{
private:
  ThreadPool(int num = N):_num(num)
  {
    pthread_mutex_init(&_mutex,nullptr);
    pthread_cond_init(&_cond,nullptr);
  }
  ThreadPool(const ThreadPool<T>& tp) = delete;  //删除构造函数
  void operator=(const ThreadPool<T>& tp) = delete; //删除赋值函数

public:
  static ThreadPool<T>* GetInstance()
  {
      if(instance == nullptr) //假如instance指针为空
      {
          LockGuard lockguard(&instance_lock); 
          if(nullptr == instance)
          {
            instance = new ThreadPool<T>();
            instance->Init();
            instance->start();
          }
      }
      return instance;
  }
  ~ThreadPool()
  {
    for(auto &t:_threads)
    {
      t.Join();
    }
    pthread_mutex_destroy(&_mutex);
    pthread_cond_destroy(&_cond);
  }
  pthread_mutex_t* Getlock()
  {
    return &_mutex;
  }

  void ThreadWait()
  {
    pthread_cond_wait(&_cond,&_mutex);  //没有任务,线程自动进入等待
  }
  void ThreadWakeUp()
  {
    pthread_cond_signal(&_cond);  //唤醒任务队列里面的线程
  }
  //判断任务队列是否为空
  bool Isempty()
  {
    return _tasks.empty();
  }
  T popTask()
  {
    T t = _tasks.front();
    _tasks.pop();
    return t;
  }
  void PushTask(const T&t)
  {
    LockGuard lockguard(&_mutex);
    _tasks.push(t); //任务入列
    ThreadWakeUp(); //唤醒线程进行工作
  }
  static void* ThreadRoutine(void* args)
  {
     //将传进来的this指针,转成我们的对象,这样即可访问里面的方法和成员变量
     ThreadPool<Task>* tp = static_cast<ThreadPool<Task> *>(args);
     while (true)
     {
       T t;
       //任务队列不为空
       {
         LockGuard lockguard(tp->Getlock());
         while(tp->Isempty())
         {
            tp->ThreadWait();  //假如没有任务,则等待
         }
          //有任务,取出对应的任务
         t = tp->popTask();
       }
       t();  //执行任务
     }
  }
  void Init()
  {
    for(int i = 0;i < _num;i++)
    {
      _threads.push_back(Thread(i,ThreadRoutine,(void*)this));
    } 
  }
  void start()
  {
    for (auto &t:_threads)
    { 
      t.Run();   //调用自定义线程里面的Run函数,创建相应的线程,并完成对应的任务
    }
  }

private:
  std::vector<Thread> _threads; //线程编号向量
  int _num;   //线程数量

  std::queue<T> _tasks;  //任务数量
  pthread_mutex_t _mutex; //锁
  pthread_cond_t _cond;   //条件变量

  static ThreadPool<T>* instance; //类对象指针
  static pthread_mutex_t instance_lock; //类对象锁
};

//对对象指针进行初始化
template <class T>
ThreadPool<T>* ThreadPool<T>::instance = nullptr;

//对类对象锁进行初始化
template <class T>
pthread_mutex_t ThreadPool<T>::instance_lock = PTHREAD_MUTEX_INITIALIZER;

锁和线程都用我们自己封装过后的版本

#pragma once

#include <iostream>
#include <pthread.h>

class Mutex
{
public:
  Mutex(pthread_mutex_t* mutex):pmutex(mutex)
  {}
  ~Mutex()
  {}
  void Lock()
  {
     pthread_mutex_lock(pmutex);
  }
  void Unlock()
  {
    pthread_mutex_unlock(pmutex);
  }
private:
   pthread_mutex_t* pmutex;
};

class LockGuard
{
public:
   LockGuard(pthread_mutex_t* mutex):_mutex(mutex)
   {
     //在创建的时候,就自动上锁
     _mutex.Lock();
   }
   ~LockGuard()
   {
     //销毁的时候,自动解锁
     _mutex.Unlock();
   }

private:
  Mutex _mutex;
};
#include <iostream>
#include <stdlib.h>
#include <pthread.h>
#include <cstring>
#include <string>
class Thread{
public:
  typedef enum
  {
     NEW = 0,
     RUNNING,
     EXITED
  }ThreadStatus;
    typedef void* (*func_t)(void*);
public:
  Thread(int num,func_t func,void* args):_tid(0),_status(NEW),_func(func),_args(args)
  {
     //名字由于还要接收用户给的编号,因此在构造函数内进行初始化
     char buffer[128];
     snprintf(buffer,sizeof(buffer),"thread-%d",num);
     _name = buffer;
  }
  ~Thread()
  {}
  //返回线程的状态
  int status()  {return _status;}
  //返回线程的名字
  std::string name() {return _name;}
  //返回线程的id
  //只有线程在运行的时候,才会有对应的线程id
  pthread_t GetTid()
  {
    if (_status == RUNNING)
    {
      return _tid;
    }
    else
    {
      return 0;
    }
  }
  //类成员函数具有默认参数this
  //将其变为static静态函数,但是会有新的问题
  static void * ThreadRun(void* args)
  {
    Thread* ts = (Thread*)args;  //此时就获取到我们对象的指针
    // _func(args);  //此时就无法回调相应的方法(成员函数无法直接被访问)
    (*ts)();
    return nullptr;
  }
  void operator()() //仿函数
  {
     //假如传进来的线程函数不为空,则调用相应的函数
     if(_func != nullptr)  _func(_args);
  }
  //线程运行
  void Run()
  {
    //线程创建的参数有四个
    //int n = pthread_create(&_tid,nullptr,_func,_args);
    int n = pthread_create(&_tid,nullptr,ThreadRun,this);
    if(n != 0)  exit(0);
    _status = RUNNING;
  }

  //线程等待
  void Join()
  {
    int n = pthread_join(_tid,nullptr);
    if (n != 0)
    {
       std::cerr << "main thread join error :" << _name << std::endl;
       return;
    }
    _status = EXITED;
  }
private:
   pthread_t _tid;    //线程id
   std::string _name; //线程的名字
   func_t _func;       //未来要回调的函数
   void*_args;
   ThreadStatus _status; //目前该线程的状态
};

现在需要编写的是Task.hpp这个文件,也就是我们创建的任务是什么
需要将对应的任务压入任务队列当中,并由我们创建的一批线程去自动执行
那Task类应该包含什么方法和成员呢?
设计的关键其实是搭配我们的线程池代码所使用,在线程池代码中,每个线程创建的时候,其实底层依旧是要调用我们的pthread_create函数,不管再怎么进行封装,只不过是需要上层传入相应的Thread_Routine方法而已.
一句话简单概括,线程创建的时候,依旧是用pthread_create函数进行创建,并需要我们传入相应的ThreadRoutine方法.
而我们的方法,就设置为取出任务队列里面的任务对象,让任务对象自己调用重载()运算符完成任务
它的本质其实就相当于我们多进程版本时编写的service函数,只不过当时是只有一个类,而现在是类与类之间进行交互

在这里插入图片描述
明白了这点后,其实Task类需要的成员和方法都非常明了

//Task.hpp
#pragma once
#include <iostream>
#include <string>
#include <unistd.h>
#include <functional>

using cb_t = std::function<void(int, const std::string &, const uint16_t &)>;
class Task
{
public:
   // 无参构造
   Task()
   {
   }
   Task(int sock, const std::string &clientip, const uint16_t &clientport,cb_t sv)
       : _sock(sock), _ip(clientip), _port(clientport), _sv(sv)
   {
   }
   void operator()()
   {
      _sv(_sock, _ip, _port);
   }
   ~Task()
   {
   }

private:
   int _sock;
   std::string _ip;
   uint16_t _port;
   cb_t _sv;
};

然后Server.hpp的代码就可以修改为下面多线程的版本

#pragma once
#include <iostream>
#include <cstring>
#include <cstdlib>
#include <unistd.h>
#include <functional>
#include <signal.h>
#include <pthread.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "err.hpp"
#include "ThreadPool.hpp"
#include "Log.hpp"
#include "daemon.hpp"

// 线程池版本
namespace ns_server
{
  const static uint16_t default_port = 8080;
  static const int backlog = 32;

  using func_t = std::function<std::string(const std::string &)>;

  class tcp_Server;
  class Thread_Data
  {
  public:
    Thread_Data(int fd, const std::string &ip, const int16_t &port, tcp_Server *ts)
        : _sock(fd), _clientip(ip), _clientport(port), _ts(ts)
    {
    }

  public:
    int _sock;
    std::string _clientip;
    int16_t _clientport;
    tcp_Server *_ts;
  };

  class tcp_Server
  {
  public:
    tcp_Server(func_t func, uint16_t port = default_port)
        : _func(func), _port(port), _quit(true)
    {
      std::cout << "server addrs: " << _port << std::endl;
    }
    void InitServer()
    {
      // 1.创建socket接口,创建对应的网络文件
      _listensock = socket(AF_INET, SOCK_STREAM, 0);
      // 假如创建失败,
      if (_listensock < 0)
      {
        std::cerr << "create socket err: " << strerror(errno) << std::endl;
        exit(SOCKET_ERR);
      }
      std::cout << "create socket success: " << _listensock << std::endl;

      // 2.绑定
      struct sockaddr_in local;
      bzero(&local, sizeof(local));  // 将结构体清零
      local.sin_family = AF_INET;    // 确定协议家族
      local.sin_port = htons(_port); // 将端口号转成大端字节序

      // 云服务器,或者一款服务器,一般不要指明某一个确定的IP;本地安装的虚拟机,或者物理机器是允许的
      local.sin_addr.s_addr = INADDR_ANY; // 随机指定任意一个ip
      if (bind(_listensock, (struct sockaddr *)&local, sizeof(local)))
      {
        std::cerr << "bind socket err: " << strerror(errno) << std::endl;
        exit(BIND_ERR);
      }
      std::cout << "bind socket success: " << _listensock << std::endl;

    // 3.监听
    if (listen(_listensock, backlog) < 0)
    {
      std::cerr << "listen socket err: " << strerror(errno) << std::endl;
      exit(LISTEN_ERR);
    }
    std::cout << "listen socket success: " << _listensock << std::endl;
    }

    void Start()
    {
      _quit = false; // 不需要退出
      signal(SIGCHLD,SIG_IGN);
      //  服务器一旦启动,就一直死循环的进行下去
      while (!_quit)
      {
        struct sockaddr_in client;
        socklen_t len = sizeof(client);
        // 4.获取连接
        int sock = accept(_listensock, (struct sockaddr *)&client, &len);
        if (sock < 0)
        {
          std::cerr << "accept socket err: " << std::endl;
          continue; // 并不会有任何报错,只是会不断循环,尝试与用户端进行连接
        }
        // 提取client信息
        std::string clientip = inet_ntoa(client.sin_addr); // 将网络ip转成点分十进制字符串
        uint16_t clientport = ntohs(client.sin_port);      // 网络字节序转成小端
        printf("获取新连接成功: %d from %d, who: %s - %d",sock,_listensock,clientip.c_str(),clientport);

        // 创建任务,并将任务压入任务队列
        Task t(sock, clientip, clientport, std::bind(&tcp_Server::service, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3));
        ThreadPool<Task>::GetInstance()->PushTask(t); // 将任务压入任务队列当中
      }
    }
    // 线程要执行的方法
    static void *ThreadRoutine(void *args)
    {
      pthread_detach(pthread_self()); // 子线程完成任务后,系统自动回收,父线程不用阻塞等待
      Thread_Data *td = static_cast<Thread_Data *>(args);
      td->_ts->service(td->_sock, td->_clientip, td->_clientport);
      delete td;
      return nullptr;
    }

    void service(int sock, const std::string &clientip, const uint16_t &clientport)
    {
      std::string who = clientip + ":" + std::to_string(clientport);
      char buffer[1024]; // 创建一个缓冲区,用来存储数据
      while (true)
      {
        ssize_t n = read(sock, buffer, sizeof(buffer) - 1); // 最后一个字符在C中默认为/0,预留一个位置
        if (n > 0)                                          // 假如成功读取到数据
        {
          buffer[n] = 0; // 缓冲区最后一个字符,填充/0
          std::string message = _func(buffer);
          std::cout << who << ">> " << message << std::endl;  //说明是谁发出的数据
          write(sock, message.c_str(), message.size()); // 往里面写数据
        }
        else if (n == 0)
        {
          // 对方将连接关闭了
          std::cout << who << " quit, me too" << std::endl;
          break;
        }
        else
        {
          std::cerr << "read error: " << strerror(errno) << std::endl;
          break;
        }
        close(sock);
      }
    }
    ~tcp_Server()
    {}

  private:
    int _listensock;
    uint16_t _port; // 端口号
    bool _quit;     // 是否要运行
    func_t _func;   // 处理信息函数
  };
}

日志组件添加

完成多线程版本改造后,还有没有地方可以进一步修改呢?
有的,就是我们的报错
在实际服务器端的报错,输出打印的内容,并不是我们现在打印的这么简单,而且要全部记录起来
从打印的内容角度看,除了要打印相应的内容,还需要对消息进行相应的分级(Lg.DEBUG 0 INFO 1 WARNING 2(告警,需要程序员注意一下) ERROR 3(一般错误,但不足以影响服务器继续运行) FATAL 4(不注意,会引发很严重的问题)等等),还有输出对应的时间,报错的地点在哪,输出该消息的时间等等
整体的输出格式应该是统一规整的,并有等级划分

日志等级 时间 文件 行 pid 消息提示

从记录的角度看,报错并不是直接打印到显示屏上这么简单,况且这么多任务,一下子全部打印到屏幕上,根本起不到调试的作用
报错记录,需要我们记录到对应的文件当中
对此,我们编写Log.hpp小组件,简单模拟日志组件,实际在工作中,日志组件会复杂的多,有几千行代码不等

#pragma once
#include <cstdio>
#include <iostream>
#include <cstring>
#include <unistd.h>
#include <ctime>
#include <stdarg.h>

const std::string filename = "Log/tcp_server.log";
enum
{
    Debug = 0,
    Info,
    Warning,
    Error,
    Fatal,
    Uknown
};
std::string ConvertStrinig(int level)
{
    switch (level)
    {
    case Debug:
        return "Debug";
    case Info:
        return "Info";
    case Warning:
        return "Warning";
    case Error:
        return "Error";
    case Fatal:
        return "Fatal";
    default:
        return "Uknown";
    }
}

static std::string GetTime()
{
    time_t curr = time(nullptr);
    struct tm *ts = localtime(&curr);
    char buffer[1024];
    snprintf(buffer,sizeof(buffer),"%d-%d-%d  %d-%d-%d",ts->tm_year + 1900, ts->tm_mon+1, ts->tm_mday,
             ts->tm_hour, ts->tm_min, ts->tm_sec);
    return buffer;
}

// 日志格式: 日志等级 时间 pid 消息体
// logMessage(DEBUG, "hello: %d, %s", 12, s.c_str()); // DEBUG hello:12, world
void LogMessage(int level, const char *format, ...)
{
    char LogLeft[1024];
    std::string level_string = ConvertStrinig(level);
    std::string current_time = GetTime();
    snprintf(LogLeft,sizeof(LogLeft),"[%s] [%s] [%d]",level_string.c_str(),current_time.c_str(),getpid());
    
    char LogRight[1024];
    va_list p;
    va_start(p,format);
    vsnprintf(LogRight,sizeof(LogRight),format,p);
    va_end(p);
    
    //对应日志存储到文件当中
    FILE* fp = fopen(filename.c_str(),"a");
    if(fp == nullptr)  return;
    fprintf(fp,"%s-%s\n",LogLeft,LogRight);
    fflush(fp); 
    fclose(fp);
}

线程池版server.hpp就可以修改为如下形式

#pragma once
#include <iostream>
#include <cstring>
#include <cstdlib>
#include <unistd.h>
#include <functional>
#include <signal.h>
#include <pthread.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "err.hpp"
#include "ThreadPool.hpp"
#include "Log.hpp"
#include "daemon.hpp"

// 线程池版本
namespace ns_server
{
  const static uint16_t default_port = 8080;
  static const int backlog = 32;

  using func_t = std::function<std::string(const std::string &)>;

  class tcp_Server;
  class Thread_Data
  {
  public:
    Thread_Data(int fd, const std::string &ip, const int16_t &port, tcp_Server *ts)
        : _sock(fd), _clientip(ip), _clientport(port), _ts(ts)
    {
    }

  public:
    int _sock;
    std::string _clientip;
    int16_t _clientport;
    tcp_Server *_ts;
  };

  class tcp_Server
  {
  public:
    tcp_Server(func_t func, uint16_t port = default_port)
        : _func(func), _port(port), _quit(true)
    {
      std::cout << "server addrs: " << _port << std::endl;
    }
    void InitServer()
    {
      // 1.创建socket接口,创建对应的网络文件
      _listensock = socket(AF_INET, SOCK_STREAM, 0);
      // 假如创建失败,
      if (_listensock < 0)
      {
        LogMessage(Fatal, "create socket err, code: %d,error string: %S", errno, strerror(errno));
        exit(SOCKET_ERR);
      }
      LogMessage(Info, "create socket success, code: %d,error string: %S", errno, strerror(errno));

      // 2.绑定
      struct sockaddr_in local;
      bzero(&local, sizeof(local));  // 将结构体清零
      local.sin_family = AF_INET;    // 确定协议家族
      local.sin_port = htons(_port); // 将端口号转成大端字节序

      // 云服务器,或者一款服务器,一般不要指明某一个确定的IP;本地安装的虚拟机,或者物理机器是允许的
      local.sin_addr.s_addr = INADDR_ANY; // 随机指定任意一个ip
      if (bind(_listensock, (struct sockaddr *)&local, sizeof(local)))
      {
        LogMessage(Fatal, "bind socket err, code: %d,error string: %S", errno, strerror(errno));
        exit(BIND_ERR);
      }
      std::cout << "bind socket success: " << _listensock << std::endl;

    // 3.监听
    if (listen(_listensock, backlog) < 0)
    {
      LogMessage(Fatal, "listen socket err, code: %d,error string: %S", errno, strerror(errno));
      exit(LISTEN_ERR);
    }
    LogMessage(Info, "listen socket success, code: %d,error string: %S", errno, strerror(errno));
    }

    void Start()
    {
      _quit = false; // 不需要退出
      // signal(SIGCHLD,SIG_IGN);
      //  服务器一旦启动,就一直死循环的进行下去
      while (!_quit)
      {
        struct sockaddr_in client;
        socklen_t len = sizeof(client);
        // 4.获取连接
        int sock = accept(_listensock, (struct sockaddr *)&client, &len);
        if (sock < 0)
        {
          LogMessage(Warning, "accept socket err, code: %d,error string: %S", errno, strerror(errno));
          continue; // 并不会有任何报错,只是会不断循环,尝试与用户端进行连接
        }
        // 提取client信息
        std::string clientip = inet_ntoa(client.sin_addr); // 将网络ip转成点分十进制字符串
        uint16_t clientport = ntohs(client.sin_port);      // 网络字节序转成小端
        LogMessage(Info, "获取新连接成功: %d from %d, who: %s - %d", sock, _listensock, clientip.c_str(), clientport);

        // 创建任务,并将任务压入任务队列
        Task t(sock, clientip, clientport, std::bind(&tcp_Server::service, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3));
        ThreadPool<Task>::GetInstance()->PushTask(t); // 将任务压入任务队列当中
      }
    }
    // 线程要执行的方法
    static void *ThreadRoutine(void *args)
    {
      pthread_detach(pthread_self()); // 子线程完成任务后,系统自动回收,父线程不用阻塞等待
      Thread_Data *td = static_cast<Thread_Data *>(args);
      td->_ts->service(td->_sock, td->_clientip, td->_clientport);
      delete td;
      return nullptr;
    }

    void service(int sock, const std::string &clientip, const uint16_t &clientport)
    {
      std::string who = clientip + ":" + std::to_string(clientport);
      char buffer[1024]; // 创建一个缓冲区,用来存储数据
      while (true)
      {
        ssize_t n = read(sock, buffer, sizeof(buffer) - 1); // 最后一个字符在C中默认为/0,预留一个位置
        if (n > 0)                                          // 假如成功读取到数据
        {
          buffer[n] = 0; // 缓冲区最后一个字符,填充/0
          std::string message = _func(buffer);
 //说明是谁发出的数据
          LogMessage(Debug, "%s#%s", who.c_str(), message.c_str());
          write(sock, message.c_str(), message.size()); // 往里面写数据
        }
        else if (n == 0)
        {
          // 对方将连接关闭了
          LogMessage(Debug, "%s quit, me too", who.c_str());
          break;
        }
        else
        {
          LogMessage(Error, "read error, %d:%s", errno, strerror(errno));
          break;
        }
        close(sock);
      }
    }
    ~tcp_Server()
    {}

  private:
    int _listensock;
    uint16_t _port; // 端口号
    bool _quit;     // 是否要运行
    func_t _func;   // 处理信息函数
  };
}

守护进程

每个进程都有自己的进程PID,类似于我们工作时每个人都有自己的工作牌号
而除了PID外,每个进程还有自己的PGID(组ID),和SID(会话ID)
类比于我们在生活中实际还分属于不同的组,与不同的公司
在这里插入图片描述

从范围上来讲,会话>=进程组>=进程
会话(SID) ,与终端直接相关联,每在Xshell中打开一个新的终端,进程所属的SID都是不同的
组ID(PGID),一个任务,可能由有个进程来完成,PGID由创建出的多个进程中的第一个被创建的进程PID来确定!

控制进程组的指令
Jobs 查看后台任务
Fg(front ground)+任务编号 把后台任务拎到前台,进行执行
后台任务不能用Ctrl+c来终止,要输入Ctrl+z,此时就会将它放到后台任务中,但状态是停止
Bg+任务编号 把前台任务拎到后台,进行执行

站在用户角度,可以把一个进程组看作一个任务,这个任务可以由多个进程来完成
而对于一个进程组(任务)来说,我们可以把它看作前台进程(任务)与后台进程(任务)
在会话中只能有一个前台任务在跑,假如后台任务提到前台,则老的前台任务就无法运行
这就解释了为什么我们打开一个终端(新的会话),死循环打印某个语句(一个前台任务),此时bash是无法解释命令行(bash也是一个前台任务)
如果登录就是创建一个会话,对于bash而言,PID = SID = PGID
而启动我们的进程,就是在当前会话中创建新的前后台任务,那么如果我们退出呢?
销毁会话,很可能会影响会话内部的所有任务
所以一般网络服务器,为了不受到用户的登录注销的影响,网络服务器----都是以守护进程的方式进行运行
通俗点讲,其实就是网络服务器程序自成一个会话

在系统接口函数中,也有将一个进程(任务),单独设置为一个任务的函数Setsid

pid_t setsid(void);

在这里插入图片描述
在这里插入图片描述

守护进程有四大任务需要完成
A.卸任公司的组长,保证自己不是公司的组长
B.忽略异常信号
C.文件描述符0,1,2要做特殊处理
D.进程的工作路径可能要更改

#pragma once

#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include "Log.hpp"
#include "err.hpp"

void Daemon()
{
    // 忽略信号
    signal(SIGCHLD, SIG_IGN);
    signal(SIGPIPE, SIG_IGN);

    // 不要成为组长,以子进程方式向下运行
    if (fork() > 0)
    {
        exit(0);
    }
    // child process
    pid_t ret = setsid();
    if (ret < 0)
    {
        LogMessage(Fatal, "deamon error, code: %d, string: %s", errno, strerror(errno));
        exit(SETSID_ERR);
    }
    // 更改目录路径
    chdir("/");

    // 文件描述符0,1,2要做特殊处理,重定向至/dev/null文件中
    int fd = open("/dev/null", O_RDWR);
    if(fd < 0)
    {
        LogMessage(Fatal, "open error, code: %d, string: %s", errno, strerror(errno));
        exit(OPEN_ERR);
    }
    dup2(fd,0);
    dup2(fd,1);
    dup2(fd,2);
    close(fd);
}

服务器初始化后,调用对应的Daemon函数,进行守护进程化即可.

//server.cc
#include "tcp_server.hpp"
#include <memory>
#include <cstdio>
using namespace ns_server;
using namespace std;


//用户使用手册
static void Usage(string proc)
{
   std::cout << "Usage:\n\t" << proc << " serverport\n" << std::endl;
}

//上层的业务处理
std::string echo(std::string request)
{
   return request;
}



// ./udp_server port
int main(int argc,char* argv[])
{   
    //假如传入的参数不是两个,而不是指定了对应的端口号,则打出对应的使用列表,并退出
    if(argc != 2)
    {
        Usage(argv[0]);
        exit(USAGE_ERR);
    }
    uint16_t port = atoi(argv[1]);
    unique_ptr<tcp_Server> tsrv(new tcp_Server(echo,port));
    
    tsrv->InitServer();
    Daemon(); //守护进程
    tsrv->Start();
    return 0;
}

此时,即便用户退出,服务器也会继续运行,并且提供的服务不会受到影响,一直会运行下去!
这就是为什么我们还能在半夜继续使用抖音,微博,微信等等各种软件,而不受影响的原因.

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

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

相关文章

MongoDB~存储引擎了解

存储引擎 存储引擎是一个数据库的核心&#xff0c;主要负责内存、磁盘里数据的管理和维护。 MongoBD的优势&#xff0c;在于其数据模型定义的灵活性、以及可拓展性。但不要忽略&#xff0c;其存储引擎也是插件式的存在&#xff0c;支持不同类型的存储引擎&#xff0c;使用不同…

C++学习笔记(22)——多态

目录 [TOC](目录) 比喻与理解1. 多态的概念2. 多态的定义及实现2.1多态的构成条件2.2 虚函数2.3虚函数的重写2.3.1 虚函数重写的两个例外&#xff1a;1. 协变(基类与派生类虚函数返回值类型不同)2. 析构函数的重写(基类与派生类析构函数的名字不同) 2.4 C11 override 和 final2…

【Git教程】(二十)外包长历史记录 — 概述及使用要求,执行过程及其实现,替代解决方案 ~

Git教程 外包长历史记录 1️⃣ 概述2️⃣ 使用要求3️⃣ 执行过程及其实现3.1 外包项目历史3.2 链接到当前活动版本库 Git 版本库会随着时间积累越来越大&#xff0c;会影响它的内存管理效率。通常在版本库中只有源 代码文件情况下&#xff0c;这点效率影响可以忽略不计。在现…

新火种AI|倒反天罡!美国名校斯坦福AI团队抄袭中国大模型

作者&#xff1a;一号 编辑&#xff1a;美美 中国大模型被抄袭&#xff0c;怎么不算是某种层面上的国际认可呢&#xff1f; 5月29日&#xff0c;斯坦福大学的一个AI研究团队发布了一个名为「Llama3V」的模型&#xff0c;号称只要 500 美元就能训练出一个 SOTA 多模态模型&am…

精酿啤酒新风尚,FENDI CLUB盛宴启幕,品质生活触手可及

随着现代人对生活品质的追求日益提升&#xff0c;精酿啤酒作为一种新兴的生活方式&#xff0c;正逐渐引领潮流。在这个背景下&#xff0c;FENDI CLUB的盛宴盛大开启&#xff0c;为广大消费者带来了一场别具一格的品质生活体验。 一、精酿啤酒的崛起 精酿啤酒以其独特的口感、…

手机卡不缴纳违约金就不给注销?实用的处理方法大全!

我手机卡都不用了&#xff0c;为何不能注销&#xff1f;而且要缴纳违约金&#xff1f;简直是无法无天&#xff01;小编在回复粉丝问题的时候&#xff0c;经常遇到这种情况&#xff0c;现在就给大家系统整理下如何处理这个问题&#xff0c;希望能帮助到大家&#xff01; 在处理不…

段子照进现实!裁员裁到大动脉,理想被传召回被裁员工…?

你一定看过类似这样的段子吧&#xff01;「公司高层换血&#xff0c;各个部门丢裁了个遍&#xff0c;终于要对财务下手&#xff0c;财务总监走之前&#xff0c;让公司补了六百万税」 还有类似这样的&#xff1a;「某公司裁员把一个销售主管裁了&#xff0c;那销售上午刚谈了个1…

vue动态加载组件import引入组件找不到组件(Error: Cannot find module)

更多ruoyi-nbcio功能请看演示系统 gitee源代码地址 前后端代码&#xff1a; https://gitee.com/nbacheng/ruoyi-nbcio 演示地址&#xff1a;RuoYi-Nbcio后台管理系统 http://218.75.87.38:9666/ 更多nbcio-boot功能请看演示系统 gitee源代码地址 后端代码&#xff1a; h…

【杂谈】AIGC之Stable Diffusion:AI绘画的魔法

Stable Diffusion&#xff1a;AI绘画的魔法 引言 在AI的世界里&#xff0c;Stable Diffusion就像一位魔法师&#xff0c;它能够将我们脑海中的幻想&#xff0c;用画笔一一描绘出来。今天&#xff0c;就让我们一探这位魔法师的奥秘&#xff0c;看看它是如何从无到有&#xff0…

Java驱动的工程项目管理系统:实现高效协作与精准管理

在工程行业的现代管理实践中&#xff0c;有效地协同工作和信息共享对于提高工作效率和降低成本至关重要。本文将深入探讨一款基于Java技术的工程项目管理系统&#xff0c;该系统采用前后端分离的架构&#xff0c;功能全面&#xff0c;旨在满足不同角色的需求&#xff0c;从项目…

【一小时学会Charles抓包详细教程】Charles 弱网测试与实战篇 (10)

&#x1f680; 个人主页 极客小俊 ✍&#x1f3fb; 作者简介&#xff1a;程序猿、设计师、技术分享 &#x1f40b; 希望大家多多支持, 我们一起学习和进步&#xff01; &#x1f3c5; 欢迎评论 ❤️点赞&#x1f4ac;评论 &#x1f4c2;收藏 &#x1f4c2;加关注 Charles 弱网测…

Message forwarding mechanism (消息转发机制)

iOS的消息转发机制 iOS的消息转发机制是在消息发送给对象时&#xff0c;找不到对应的实例方法的情况下启动的。消息转发允许对象在运行时处理无法识别的消息&#xff0c;提供了一种动态的、灵活的消息处理方式。 消息转发机制主要分为三个阶段&#xff1a; 动态方法解析快速…

基于振弦采集仪的土木工程安全监测技术研究

基于振弦采集仪的土木工程安全监测技术研究 随着土木工程的发展&#xff0c;安全监测成为了非常重要的一部分。土木工程的安全监测旨在及早发现结构的变形、位移、振动等异常情况&#xff0c;以便及时采取措施进行修复或加固&#xff0c;从而保障工程的安全运行。振弦采集仪作…

2024第26届大湾区国际电机博览会暨发展论坛

2024第二十六届大湾区国际电机博览会 暨发展论坛 2024第26届大湾区国际电机博览会暨发展论坛 The 26th Greater Bay Area International Motor Expo and Development Forum 时间&#xff1a;2024年12月4-6日 地址&#xff1a;深圳国际会展中心&#xff08;宝安新馆&#x…

【Vue】普通组件的注册使用-全局注册

文章目录 一、使用步骤二、练习 一、使用步骤 步骤 创建.vue组件&#xff08;三个组成部分&#xff09;main.js中进行全局注册 使用方式 当成HTML标签直接使用 <组件名></组件名> 注意 组件名规范 —> 大驼峰命名法&#xff0c; 如 HmHeader 技巧&#xf…

zdppy_api 中间件请求原理详解

单个中间件的逻辑 整体执行流程&#xff1a; 1、客户端发起请求2、中间件拦截请求&#xff0c;在请求开始之前执行业务逻辑3、API服务接收到中间件处理之后的请求&#xff0c;和数据库交互&#xff0c;请求数据4、数据库返回数据5、API处理数据库的数据&#xff0c;然后给客户…

【线性代数】SVDPCA

用最直观的方式告诉你&#xff1a;什么是主成分分析PCA_哔哩哔哩_bilibili 奇异值分解singular value decomposition&#xff0c;SVD principal component analysis,PCA 降维操作 pca就是降维后使得信息损失最小 投影在坐标轴上的点越分散&#xff0c;信息保留越多 pca的实现…

Springboot二屯村钓鱼场管理系统的设计-计算机毕业设计源码58167

摘 要 在互联网时代的来临&#xff0c;电子商务的骤起&#xff0c;一时间网络进行购物这一形式备受欢迎&#xff0c;到现在&#xff0c;网购更是普及。现如今各个行业也通过网购的方式来进行拓展业务&#xff0c;增加企业的知名度以及提升业绩&#xff0c;满足了用户像网购一样…

懒人开发者的福音,轻松开发应用无需搭建服务!

近日&#xff0c;一款轰动开发圈的神器正以“太硬核了&#xff01;疯传开发圈&#xff01;”的口碑迅速走红&#xff0c;那就是Memfire Cloud&#xff01;这款一站式开发应用&#xff0c;不仅让懒人开发者尽享便利&#xff0c;更为开发者们带来了前所未有的开发体验。 对于懒人…

windows操作系统提权之服务提权实战rottenpotato

RottenPotato&#xff1a; 将服务帐户本地提权至SYSTEM load incognito list_tokens –u upload /home/kali/Desktop rottenpotato.exe . execute -Hc -f rottenpotato.exe impersonate_token "NT AUTHORITY\SYSTEM" load incognito 这条命令用于加载 Metasploi…