【C++】网络套接字编程(一)

我们知道,我们任何操作都是在应用层操作的,之后再由应用层向下传递,所以,在认识应用层之前,我们应该先了解如何使用应用层中提供的接口,也就是我们常说的 套接字编程。

1. 预备知识

在接触套接字之前,我们先要对一些知识有一些感性的认识:

1.1 源IP地址 与 目的 IP地址

在IP数据报头中,有两个IP地址,一个是 源IP地址,一个是 目的IP地址。

举个简单的例子,唐僧的源IP地址是 长安,目的IP地址 是 西天。唐僧就是一段数据。

在两台主机之间,我们把数据从一台机器搬迁到另一台机器,并不是我们的目的,而是希望对端主机在接受数据之后,交付给上层处理,甚至返回相应的数据。 简单来说,唐僧去西天是为了拿到经书,而不是单纯的"旅游".


1.2端口号 与 目的端口号

1.2.1 端口号

端口号(port)是传输层协议的内容:

  1. 端口号是一个2字节16位的整数
  2. 端口号用来标识一个进程,告诉操作系统,当前数据应该交给哪一个进程去处理
  3. 一个进程可以绑定多个端口,但是一个端口只能绑定一个进程。

ip可以标识互联网中的唯一一台主机(硬件),而post标识了一台主机标识一台主机上面的唯一一个进程。所以 我们可以通过 ip:port的形式找到互联网中唯一的一个进程,换句话说,网络通信的本质是服务器之间的进程通信。

1.2.2 端口号 与 进程ID

学习过 操作系统的小伙伴知道,pid可以标识唯一的一个进程。此时我们端口号也也可以标识唯一的进程。

虽然说两者的功能是类似的,但是并不是说端口号是冗余的设计。举个例子,我们在不同的场景下会使用不同的标识,比如 身份证,学生证,会员卡等等…我们应该理解为 学生证是校园体系下的”身份证“,”会员卡“是超市体系下的"身份证",而端口号是网络体系下的进程身份证。


1.3 TCP 与 UDP

在这里我不会详细解释TCP和UDP这两种协议,一两句也无法理清。这里我想提到的一点在于,TCP和UDP不存在谁好谁坏的问题,也就是我们常常听到的"TCP可靠性大于UDP",但是,UDP的速度是大于TCP的。所以,这不是好坏问题,而是 适不适合的问题,在不同的场景下,我们选择合适的协议才是好的。


1.4 网络字节序

我们知道,内存和磁盘文件中的多字节数据相对于内存地址 有大端和小端的区分,同理,网络数据流同样有大小端之分,那么如何定义网络数据流的地址?

  1. 发送主机通常将发送缓冲区中的数据按内存地址从低到高的顺序发出;接受主机把从网路上接受到的自己依次保存在接受缓冲区,也是按照内存从低到高的顺序。因此,网络数据流的地址这样规定: 先发出的是低地址,后发出的数据是高地址.
  2. TCP/IP协议规定,网络数据流应采用大端字节序,即低地址高字节
  3. 如果当前发送主机是小端,就需要先将数据转化位大端,否则就忽略,直接发送即可。

为了使网络程序具有可移植性,使同样的C代码在大端和小端计算机上编译后能够正常运行,可以调用一下库函数进行网络字节序和主机字节序的转换。

在这里插入图片描述

这些函数并不难理解,h标识host,n表示network,l表示32位长整数,s表示16位短整数。
以htonl为例,表示将32位的长整数从主机字节序转化为网络字节序。如果主机是小端字节序,这些函数将参数做相应的大小端转换后返回,如果主机是大端字节序,则不做任何操作,将参数直接返回。


2. socket编程

2.1 socket 常见API

// 创建 socket 文件描述符 (TCP/UDP, 客户端 + 服务器)
int socket(int domain, int type, int protocol);
// 绑定端口号 (TCP/UDP, 服务器)
int bind(int socket, const struct sockaddr *address,
socklen_t address_len);
// 开始监听socket (TCP, 服务器)
int listen(int socket, int backlog);
// 接收请求 (TCP, 服务器)
int accept(int socket, struct sockaddr* address,
socklen_t* address_len);
// 建立连接 (TCP, 客户端)
int connect(int sockfd, const struct sockaddr *addr,
socklen_t addrlen);

2.1.1 socket 创建套接字

接下来我们写一个简单的udp网络程序,来实际使用一下这些接口。

  1. 函数原型

在这里插入图片描述
2. 参数:

  • domain
    domain代表协议家族,我们在这里选择IPv4,也就是AF_INET.
    在这里插入图片描述

  • type
    套接字具有具体的种类,常用的是前两种 SOCK_STREAM,SOCK_DGRAM(用户数据包套接字).
    如果选择SOCK_STREAM,就表示tcp, 选择SOCK_DGRAM,就表示 udp
    在这里插入图片描述

  • protocol
    该协议指定要与套接字一起使用的特定协议,但是通常我们只选择一个协议就够了,所以这里通常我们填写0就行。

  1. 返回值

sockect函数的返回值是一个文件描述符,准确来说是网络数据结构的文件描述符。我们可以直接按文件理解。


2.1.2 bind 绑定

  1. 作用
    这里绑定指将之前创建好的套接字 绑定好 协议家族,端口,ip等。
  2. 函数原型
    在这里插入图片描述
  3. 返回值
    成功返回0,失败返回1
    在这里插入图片描述

2.1.3 recefrom 接受数据

为了接受对端的数据,所以我们需要调用接口 recefrom.

  • 函数原型:
    在这里插入图片描述
  • 参数
  1. sockfd
    套接字编号
  2. buf 和 len
    用户缓冲区 以及其长度
  3. flags
    读数据的方式,一般设置为0
  4. src_addr 和 addrlen
    这两个参数是 输入输出型参数,在输入方面,获取到了对端的socket信息的缓冲区以及缓冲区长度; 在输出方面,输出对端的socket信息 和 socket长度.

-返回值
返回接受的字符的大小,失败则返回-1.


2.1.4 sendto 发送数据

当我们向向对端发送数据的时候,我们可以调用 sendto

  • 函数原型
    在这里插入图片描述

  • 参数
    所有参数与 recvfrom 相同

  • 返回值
    返回发送的字符的大小,失败则返回-1.


2.1.5 listen 监听(tcp)

在这里插入图片描述

2.1.6 accept 获取连接套接字(tcp)

在这里插入图片描述

  • 参数

    1. sockfd
      套接字编号
    2. src_addr 和 addrlen
      这两个参数是 输入输出型参数,在输入方面,获取到了对端的socket信息的缓冲区以及缓冲区长度; 在输出方面,输出对端的socket信息 和 socket长度.
  • 返回值:返回值是一个套接字
    在这里插入图片描述


2.1.7 connect 发起连接请求

改接口由tcp客户端使用,用来请求与服务端建立连接。参数与之前类似,不再过多赘述。
在这里插入图片描述


2.2 sockaddr结构

socket API 是一层一层抽象的网络编程接口,适用于各种底层网络协议,如IPv4,IPv6,以及之后我们会讲解的UNIX Domain Socket,如此多的网络协议,地址格式自然存在差异。

那么如何抹平这些差异呢,为此,早期网络工程师设计出了一套数据结构 sockaddr,保证一套接口 兼容多协议。
在这里插入图片描述
IPv4 和 IPv6的地址格式定义在头文件 netinet/in.h,IPv4地址用 sockaddr_in 表示,其中包含 16位地址类型,16位端口号和32位IP地址。

IPv4、IPv6地址类型分别定义为常数AF_INET、AF_INET6. 这样,只要取得某种sockaddr结构体的首地址,不需要知道具体是哪种类型的sockaddr结构体,就可以根据地址类型字段确定结构体中的内容.

socket API可以都用struct sockaddr *类型表示, 在使用的时候需要强制转化成sockaddr_in; 这样的好
处是程序的通用性, 可以接收IPv4, IPv6, 以及UNIX Domain Socket各种类型的sockaddr结构体指针做为
参数。


虽然说socket api的接口是 sockaddr, 但是在我们基于IPv4编程的时候,实际使用的数据结构是sockaddr_in,这个结构里面由三部分信息:

  1. 地址类型 (sin_family)
  2. 端口号 (sin_port)
  3. ip地址 (sin_addr)

眼见为实,我们可以看下 centos7中的sockaddr_in 结构体。
在这里插入图片描述
其中我们发现 sin_addr的类型是 in_addr,实际上in_addr用来表示一个IPv4的IP地址。其实就是一个封装过的32位的整数(如下图)
在这里插入图片描述


2.3 简单的UDP网络程序

2.3.1 udp服务端

  1. 创建套接字
int sock=socket(AF_INET,SOCK_DGRAM,0);
if(sock<0){
    
                            
   std::cout<<"socket error "<<std::endl;
   return 2;
}
  1. 绑定
    在绑定之前,我们先要创建struct sockaddr_in 结构体并且填充数据。
struct sockaddr_in local;
memset(&local,0,sizeof local); //清空结构体
local.sin_family = AF_INET;
local.sin_port = htons(8081);
local.sin_addr.s_addr = htonl(INADDR_ANY);
if(bind(sock,(struct sockaddr*)&local,sizeof(local))<0){
    
    
   std::cout<<"bind error"<<std::endl;
   return 3;
}

在绑定IP的时候,我们需要注意两点:

  • 一个IP本质是一个32位的整数,我们可以用4个字节保存,但是我们看到的ip形式,比如:101.35.3.69,是一种叫做 ”点分十进制“的字符串风格IP,此种字符串所占字节数肯定更多,所以实际上,在网络中传输的ip一定是4字节的形式,因此,我们需要将 点分十进制 转化为 4字节形式。

    如何转化?使用库函数 in_addr_t,可以将点分十进制 ip格式转化位4字节ip形式
    在这里插入图片描述

  • 如果你和我一样的话,使用的是云服务器,那么在绑定的时候,一般不能绑定任何明确的ip,因为这个公网ip并不属于我们,具体原因以后会讲。

    此时我们推荐使用INADDR_ANY(代表0值),它可以绑定你的机器上的所有ip。
    在这里插入图片描述

  • Server 端一定要进行绑定,但是client端不需要明确的绑定。

    1. server端需要绑定的原因在于 client :server = n:1 (数量比),server为别人提供服务,就需要尽可能的将自己暴露出去(即 ip+port),且必须是”稳定“的(不能够轻易改变的,尤其是端口号).
    2. client端不需要明确的绑定的原因在于 虽然client 没有指定 port会导致不能够与server进行通信,但是如果我们对client 进行了明确的绑定,当client端被别的程序占用的时候,你的client就无法启动。所以,我们一般不自己bind,而是由OS随机帮我们查找端口。

  1. 时间循环 (死循环)

服务器是一个软件程序,周而复始,一直运行,永远不退出。 在这个循环中我们处理对端发来的信息。

   char message[1024];  
   for(; ; ){
    
                                                                                                                                                                                                                                                
      memset(message,0,sizeof(message));
      struct sockaddr_in peer;
      socklen_t len =sizeof(peer); //peer 远端
      ssize_t s=recvfrom(sock,message,sizeof(message)-1,0,(struct sockaddr*)&peer,&len); 
      if(s>0){
    
    
      	//处理数据
      }else{
    
    
      	//接受出错
      }
   }

udp_server 完整代码:

#include<iostream>
#include<time.h>
#include<unistd.h>
#include<cstring>
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>

#define PORT 8081

void Usage(std::string proc){
    
    
  std::cerr<<"Usage;"<<"\n\t"<<proc<<"local_port"<<std::endl;
}

int main(int argc,char* argv[])
{
    
    
  if(argc!=2){
    
    
    Usage(argv[0]);
    return 1;
  }
   int sock=socket(AF_INET,SOCK_DGRAM,0);
   if(sock<0){
    
                                  
     std::cout<<"socket error "<<std::endl;
     return 2;
   }
   
   struct sockaddr_in local; 
   memset(&local,0 ,sizeof(local));
   local.sin_family = AF_INET;
   local.sin_port = htons(atoi(argv[1]));
   
   local.sin_addr.s_addr= htonl(INADDR_ANY);
   if(bind(sock,(struct sockaddr*)&local,sizeof(local))<0){
    
    
     std::cout<<"bind error"<<std::endl;
     return 3;
   }
   
   char message[1024];
   
   for(; ; ){
    
    
     struct sockaddr_in peer;
     socklen_t len =sizeof(peer); //peer 远端
     ssize_t s=recvfrom(sock,message,sizeof(message)-1,0,(struct sockaddr*)&peer,&len); 
     if(s>0){
    
    
       message[s]='\0';
       std::cout<<"client# "<<message<<std::endl;
       std::string echo_message = message;
       echo_message +=" _server_";
       echo_message += std::to_string((long long)time(nullptr));   
       sendto(sock,echo_message.c_str(),echo_message.size(),0,(struct sockaddr*)&peer,len);

     }
     else{
    
    
       //TODO
        
     }
   }

   
   close(sock);
   return 0;
}


2.3.2 udp客户端

客户端代码实现:

#include<iostream>
#include<unistd.h>
#include<cstdio>
#include<cstring>
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>

// ./udp_client desc_ip desc_port

void Usage(std::string proc){
    
    
   std::cerr<<"Usage;  "<<"\n\t"<<proc<<"desc_ip desc_port"<<std::endl;//使用手册
}
//使用命令行参数
int main(int argc,char*argv[])
{
    
    
  if(argc != 3){
    
    
      Usage(argv[0]);
      return 1;
  }

   int sock = socket(AF_INET,SOCK_DGRAM,0);
   if(sock<0){
    
    
     std::cout<<"socket error"<<std::endl;
     return 2;
   }
   // bind client 端,不需要明确的绑定
   //需不需要bind??需要。
   //不需要用户主动去bind,实际上,在sendto的时候,OS会自动随机给client bind 端口号
   
   char buffer[1024];
   
   //对端主机(服务器)的相关信息
   struct sockaddr_in desc;
   memset(&desc,0,sizeof(desc));
   desc.sin_family=AF_INET;
   desc.sin_port= htons(atoi(argv[2]));
   desc.sin_addr.s_addr=inet_addr(argv[1]);

   for( ; ;){
    
    
     std::cout<<"Please Enter# ";
     fflush(stdout);
     buffer[0]=0; //将字符串清零
     ssize_t size=read(0,buffer,sizeof(buffer)-1);//从标准输入读取数据
       if(size>0){
    
    
         buffer[size-1]=0; //去掉最后的'\n'
         sendto(sock,buffer,strlen(buffer),0,(struct sockaddr*)&desc,sizeof(desc));
                                                                                              
         struct sockaddr_in peer;
         socklen_t len =sizeof(peer);
         ssize_t s = recvfrom(sock,buffer,sizeof(buffer)-1,0,(struct sockaddr*)&peer,&len);//peer暂时不用
         if(s>0){
    
    
           buffer[s]=0;
           std::cout<<"echo# "<<buffer<<std::endl;
         }
       }
   }
   
   close(sock);
   return 0;
}

我们将udp_server 启动,通过 netstat -nlup 可以查看 所有udp进程,发现udp_server 正常运行:
在这里插入图片描述
我们通过本地环回测试一下能否正常通信:
在这里插入图片描述


2.3.3 udp 实现简单的远程shell

上面我们已经讲了udp的实现方法,但是只是传输文字稍显无聊与乏味,所以这里我们尝试借助udp实现一个简单远程的shell:通过客户端输入命令来控制 服务器上的shell并显示结果。

我们只需要将之前的udp_server简单改造即可:

最开始我的想法是 对客户端发来的数据先进行分割,创建子进程,重定向文件stdout 到套接字sock,最后程序替换,指向命令,由于文件描述符重定向,所以运行结果会回显在客户端上。

但是sock是用户数据报(一个一个报文),与普通文件的流式传输不一样,所以不能重定向。(如下)

for(; ; ){
    
    
     struct sockaddr_in peer;
     socklen_t len =sizeof(peer); //peer 远端
     ssize_t s=recvfrom(sock,message,sizeof(message)-1,0,(struct sockaddr*)&peer,&len); 
     if(s>0){
    
    
       char *command[64]={
    
    0};
       command[0]=strtok(message," ");
       int i=1;
       while(command[i]=strtok(nullptr," ")){
    
    
           i++;
       }
       if(fork() == 0){
    
    
           dup2(sock,1);
           execvp(command[0],command);
           std::cerr <<"client message# "<<command<<std::endl;
           exit(4);
       }
       
     }
     else{
    
    
       //TODO
        
     }

所以这里我们要引入一个新的接口:popen

popen的第一个参数可以执行任意命令,结果以文件指针的方式返回。其原理也是创建子进程并程序替换。
在这里插入图片描述

代码如下:

for(; ; ){
    
    
     struct sockaddr_in peer;
     socklen_t len =sizeof(peer); //peer 远端
     ssize_t s=recvfrom(sock,message,sizeof(message)-1,0,(struct sockaddr*)&peer,&len); 
     if(s>0){
    
    
       FILE* in =popen(message,"r");
       if(in == nullptr){
    
    
           continue;
       }
       std::string echo_message;
       char line[128];
       while(fgets(line,sizeof(line),in)){
    
    
           echo_message+=line; //读取运行结果存储到echo_message
       }
       sendto(sock,echo_message.c_str(),echo_message,0,(struct sockaddr*)&peer,len);
     }
     else{
    
    
       //TODO
        
     }

2.4 简单的TCP网络程序

2.4.1 tcp服务端

创建tcp_server 的基本步骤:

  1. 创建套接字
  2. 绑定
  3. 监听
  4. 获取连接
  • tcp_server.cc
#pragma once 
#include<iostream>
#include<string>
#include<cstring>
#include<strings.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>

namespace ns_tcpserver{
    
    

  typedef void (*handler_t)(int);//函数指针类型


  const int backlog=5;

  class TcpServer{
    
    
    private:
      uint16_t port;
      int listen_sock;
    public:
      TcpServer(int _port):port(_port),listen_sock(-1)
    {
    
    }
      void InitTcpServer(){
    
    
        
         listen_sock=socket(AF_INET,SOCK_STREAM,0);
         if(listen_sock<0){
    
    
           std::cout<<"socket error"<<std::endl;
           exit(2);
         }
         
         struct sockaddr_in local;
         bzero(&local,sizeof(local));
         local.sin_family=AF_INET;
         local.sin_port=htons(port);
         local.sin_addr.s_addr=INADDR_ANY;

         if(bind(listen_sock, (struct sockaddr*)&local,sizeof(local))<0){
    
    
           std::cerr<<"bind error"<<std::endl;
           exit(3);
         }
         //3.监听,tcp 协议是面向连接的,即如果要正式传递数据之前,需要先建立链接
         //目的: 允许client 来连接server
        
         if(listen(listen_sock,backlog)<0){
    
    
           std::cerr<<"listen error"<<std::endl;
           exit(4);
         }

      }
      //自己设计的回调机制
      void Loop(handler_t handler)
      {
    
    
        while(true){
    
    
          struct sockaddr_in peer;
          socklen_t len=sizeof(peer);
          //1. 获取连接
          int sock = accept(listen_sock,(struct sockaddr*)&peer,&len); //输入输出型参数
          if(sock<0){
    
    
            std::cout<<"warning: accept error"<<std::endl;
            continue; //不能使用break;

          }

          //验证一下fd值
           std::cout<<"debug: sock->"<<sock<<std::endl;
           uint16_t peer_port = ntohs(peer.sin_port);
           std::string peer_ip = inet_ntoa(peer.sin_addr);

           //验证一下对端的socket信息中,ip,port.
           std::cout<<"debug: "<<peer_ip<<":"<<peer_port<<std::endl;

          //使用socket进行通信
           
          //5. 处理连接
          handler(sock);

          //6. 关闭链接--暂时
          //close(sock);
           
          
        } 
      }
      ~TcpServer(){
    
    
        if(listen_sock>=0) close(listen_sock);  
      }
  };


}

  • server.cc
#include"tcp_server.hpp"//提供网络连接功能
#include"handler.hpp"    //提供网络sock的处理功能

static void Usage(std::string proc)
{
    
    
   std::cerr<<"Usage:"<<"\n\t"<<proc<<"port"<<std::endl;//使用提示
}

// ./server port
int main(int argc,char *argv[])
{
    
    
  //这是一个简单的命令行参数的验证
  if(argc!=2){
    
    
    Usage(argv[0]);
    return 0;
  }
  uint16_t port = atoi(argv[1]);
  ns_tcpserver::TcpServer *svr = new ns_tcpserver::TcpServer(port);
  svr->InitTcpServer();
  svr->Loop(ns_handler::HandlerSock_V4);
  
  //svr->Loop(ns_handler::HandlerSock_V1);
  //svr->Loop(ns_handler::HandlerSock_V2);
  //svr->Loop(ns_handler::HandlerSock_V3);
  return 0;
}

2.4.2 tcp客户端

  • tcp_client.hpp
#pragma once

#include<iostream>
#include<string>
#include<cstring>
#include<strings.h>
#include<unistd.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<sys/types.h>
#include<arpa/inet.h>


namespace ns_tcpclient{
    
    
  class TcpClient{
    
    
     private:
       std::string desc_ip;//client 要访问的对端服务器的IP地址
       uint16_t desc_port; //client要访问的对端服务器的port端口号
       int sock;
     public:
       TcpClient(std::string _ip, uint16_t _port):desc_ip(_ip),desc_port(_port),sock(-1)
       {
    
    }

       void InitTcpClient()
       {
    
    
         sock=socket(AF_INET,SOCK_STREAM,0);
         if(sock<0){
    
    
           std::cerr<<"socket error"<<std::endl;
           exit(2);
         }

         //2. client不需要自己bind,在你发起链接的时候,OS会自动给你进行相关的绑定!
         //3. client不需要listen
         //4. client不需要accept 
         

       }
       //tcp是面向连接的!client要通信之前必须先连接
       void Start()
       {
    
    
         //填充对端服务器的socket信息
         struct sockaddr_in svr;
         bzero(&svr,sizeof(svr));
         svr.sin_family=AF_INET;
         svr.sin_port=htons(desc_port);
         svr.sin_addr.s_addr=inet_addr(desc_ip.c_str());
        
         //2. 发起链接请求
         if(connect(sock,(struct sockaddr*)&svr,sizeof(svr))==0){
    
    
           std::cout<<"connect success ..."<<std::endl;
         }
         else{
    
    
           std::cout<<"connect failed ..."<<std::endl;
           return;
         }

         //3. 完成业务逻辑
         while(true){
    
    
           char buffer[1024]={
    
    0};

           std::cout<<"请你输入#";
           fflush(stdout);
           ssize_t s = read(0,buffer,sizeof(buffer)-1);
           if(s>0){
    
    
             buffer[s-1]=0;//去除回车值
             write(sock,buffer,strlen(buffer));

             ssize_t rs = read(sock,buffer,sizeof(buffer)-1);
             if(rs>0){
    
    
               buffer[rs]=0;
               std::cout<<buffer<<std::endl;
             }
             else{
    
    
               std::cout<<"server close..."<<std::endl;
               break;
             }
           }
         }

       }
       ~TcpClient()
       {
    
    
         if(sock>=0)close(sock);
       }

  };
}


  • client.cc
#include"tcp_client.hpp"


static void Usage(std::string proc){
    
    
    std::cerr<<"Usage: "<<"\n\t"<<proc<<"svr_ip svr_port"<<std::endl;
}

// ./tcp_client peer_ip peer_port
int main(int argc,char* argv[])
{
    
    
  if(argc !=3){
    
    
    Usage(argv[0]);
    return 1;
  }
  std::string ip=argv[1];
  uint16_t port =atoi(argv[2]);
  
  ns_tcpclient::TcpClient cli(ip,port);
  cli.InitTcpClient();
  cli.Start();
  return 0;
  
  
}


  • handler.hpp

这里我们设计tcp的时候将tcp的启动 与 tcp中的处理逻辑解耦,单独拿出来写。

这里我们会介绍几种对设计方式,并研究其优缺点,并给出较优的解决方案。

  1. 方案1

可以发现,下面的代码就是一份单线程的代码,所以同一时间内只可以一个用户与server进行通信,所以这个方案只适合1对1的场景,几乎没有实际应用价值。

namespace ns_handler{
    
    
   using namespace ns_tcpserver;
#define SIZE 1024

  void HandlerHelper(int sock)
  {
    
    
    while(true){
    
    
      char buffer[1024];
      ssize_t s=read(sock,buffer,sizeof(buffer)-1);
      if(s>0){
    
    
        //read sucess
        buffer[s]=0;
        std::cout<<"client# "<<buffer<<std::endl;
        std::string echo_string =buffer;
        if(echo_string == "quit")break;
        echo_string+="[server say]";
        write(sock,echo_string.c_str(),echo_string.size());//将读到的内容加工后回显
      }
      else if(s==0){
    
    
        // 对端链接关闭
        std::cout<<"client quit..."<<std::endl;
      }
      else{
    
    
        //读取失败--暂时不考虑
        std::cerr<<"read error"<<std::endl;
      }
   }
  }
  
  //单进程
  void HandlerSock_V1(int sock){
    
    
    HandlerHelper(sock);
  }
 }
  1. 方案2
    既然单进程少了,那么不难想到,只要创建多个进程就行了。

思路:每当有一个链接到来,我们就fork出一个子进程,让这个子进程去处数据(代码如下)。
问题:在子进程运行结束退出之前,我们的父进程都必须保证等待状态(为了避免僵尸进程的问题),那么在这个过程中server是无法进行新的回调的(Loop),这样显然是不合理的。

void HandlerSock_V2(int sock){
    
    
    if(fork() == 0){
    
    
       //子进程
       HandlerHelper(sock);
    }
    //父进程 等待
    waitpid(-1,nullptr,0);
  }

如何解决?方法有很多,这里我介绍两种:

  • 我们可以在父进程中利用信号 signal (SIGCHLD,SIG_IGN) ,此后父进程不必再等待子进程.
  • 采用下面的代码设计:
void HandlerSock_V2(int sock){
    
    
    if(fork() == 0){
    
    
      //child
      if(fork()>0){
    
    
        //child
        exit(0);
      }
      //grandson ,孤儿进程,会被操作系统领养
      HandlerHelper(sock);
      exit(0);
    }
    
    waitpid(-1,nullptr,0);
  }

这种双fork的设计第一看可能会比较奇怪,但是确实是一种巧妙利用进程特性的方式: 第一次fork,我们获取到子进程,再次fork,获取到孙子进程,同时让子进程退出,父进程停止等待。此时剩下的孙子进程由于子进程的退出变成孤儿进程,故被OS领养,此时孙子进程去处理任务,执行完成之后被OS回收。

使用这种方式,就避免了父进程的等待,也避免了僵尸进程的产生,可以说是优雅的设计。


  1. 方案3
    方案2确实是一个比较好的方法,但是创建子进程的代价是比较高的,尤其是当待处理任务比较简单,短时间内创建进程又释放稍显浪费,所以我们可以使用多线程的方式来缓解这一问题。
  void* thread_routinue(void* args)
  {
    
    
    int sock=*(int*)args;
    delete (int*)args;
    pthread_detach(pthread_self());//线程分离
    HandlerHelper(sock);//执行任务
    close(sock);
    return nullptr;
  }

  void HandlerSock_V3(int sock)
  {
    
    
    pthread_t tid;
    int*p=new int(sock);
    pthread_create(&tid,nullptr,thread_routinue,p); //创建子线程

  }

  1. 方案4

方案四是使用线程池来调度管理线程。

  • 在进行代码的实现之前,我们先比较一下4中实现方案的特点。
方案 优点 缺点
单进程 稳定,具有独立性 不能多人同时通信
多进程 稳定,具有独立性 链接来了,才创建进程,而且数量没有上限
多线程 轻量化 1.链接来了,才创建线程,而且数量没有上限 2.健壮性不足,一损俱损
线程池 轻量化,数量有上限

这里的数量上限是什么意思呢? 一旦系统中的进程或者线程极度增多,进程或者线程在系统内切换的成本增加,切换的周期变长,所我们不能让进程/进程无限制的增长。


class task{
    
    
    private:
      int sock;
    public:
      task();
      task(int _sock):sock(_sock)
      {
    
    }
      //设置仿函数
      void operator()()
      {
    
    
        std::cout<<"当前处理的线程id是"<<pthread_self()<<std::endl;
        HandlerHelper(sock);
        close(sock);
      }
      ~task(){
    
    }

}; 

void HandlerSock_V4(int sock){
    
     
	ThreadPool<task>::get_instance(5)->PushTask(task(sock)); //容量为5的线程池
}

猜你喜欢

转载自blog.csdn.net/qq_53268869/article/details/124983709