1.协议初级理解

(1)协议是一种约定,协议双方规定一种传输规则,从而可以对网络信息解码

协议不仅可以存在于远程设备之间,也可以存在于单台计算机内部

(2)协议分层:协议会进行分层设计

eg:人与人通过电话通信
主要分为语言层和通信设备层,语言层是规定好通信双方所用语言类型,通信设备层是规定好设备之间的交流方式

(3)协议诞生的原因:
由于远程数据传输可能会出现数据丢失,路由器定位,数据解析使用等问题,所以出现了用于解决这些问题的解决方法,而现在常用的解决方法就是TCP/IP协议

(4)OSI七层模型:
这种模型是用于给协议进行上层指导的一种指导方案,而TCP/IP协议就是基于OSI七层模型进行落地实现的远程传输解决方法

总共分为七层:

物理层:主要是硬件厂商规定的高低电频等信号协议

数据链路层:互联设备之间的传输和识别,有个设备叫调制解调器,可以用于将模拟信号转换为数字信号

网络层:地址管理与路由选择,我们家里的路由器就是网络层的硬件实现,主要管理网络数据传输的路径选择

传输层:输出接收两方节点之间的数据可靠性管理,主要保证起点和终点的数据传输可靠

其中前面这四层都是保证数据正确传输到位,后面的三层则是保证数据进行正确处理

(5)TCP/IP五层模型

由于工程师实际上实现网络协议的时候,发现会议层和表现层无法放在内核中,所以就把会议层,表示层,应用层都合并为了应用层,放在内核外

1.1网络协议与操作系统的关系

无论是对于windows还是linux操作系统,他们的网络协议栈都是相同的,都是基于OSI七层模型的实际落地实现版本(便于实现不同OS之间的通信)

网络协议栈:

对于网卡层:主要由硬件生产产商负责进行硬件的协议录入

对于数据链路层:主要由网卡制造厂商的程序员负责进行驱动写入

对于网络层和传输层(内核层):是由操作系统内核管理程序员进行实现的

对于应用层(软件层):由于应用层的更新是依据用户需求变更进行迭代的,而用户的需求的变化是非常快的,所以应用层才不能放在内核中,因为内核的迭代是较为缓慢的

注意:
1.最重要的部分是操作系统内核,所以内核所管理的传输层(TCP)和网络层(IP)协议就作为了命名的核心,整个网络协议栈就叫做TCP、IP协议栈

2.协议本质是一种传输统一规范,而协议的OS落地实现是基于C语言结构体的,比如windows和linux操作系统都用C语言实现了同一协议的结构体,那么他们之间的数据传输与解码就可以统一,在宏观层面上就可以说他们是统一协议的

eg:windows和linux系统中都定义了一个结构体

struct data//windows               struct data//linux
{                    ------>       {
   int a;                             int a;     
   int b;                             int b;
   int c;                             int c;
};                                 };

然后在windows中我们给a,b,c变量都填充了数据,并打包发送给linux。linux接收数据后,根据数据接收协议进行解码,其实就是按照data结构体定义的数据类型进行解读,于是就读取了三个int数据,成功解析

1.2网络传输流程

1.2.1局域网网络传输流程

当两台主机处于同一个局域网中,他们可以直接通信,可是为了表示各个主机的唯一性,我们需要类似身份证功能的东西来标识每台局域网中的主机,所以引入了MAC地址

MAC地址:

1.用于标识数据链路层相连的节点,地址占据6字节,以16进制数字加冒号来标识

eg:08:00:27:03:fb:19

2.MAC地址在网卡出厂的时候就确定了,绝大多数情况下是唯一的

3.查看linux中的MAC地址,可以使用ifconfig命令

ether后面接的就是MAC地址:fa:16:3e:fe:e8:6e

以太网传输:

正常单数据传输流程:

在局域网中,主机A发送一个发送目的地为MACE的data数据,对于主机E,他经过数据接收和验证之后成功解码了对应data数据,然后对于其他主机,例如主机B,他也会检测到该数据,然后进行验证(验证失败),不对该数据进行解码分析

疑问:可是一般情况下不只有一个信息发送源在进行信息发放,这时候会发生什么,如何处理?
假如出现多个信息同时发送,此时就会出现信息碰撞(以太网又叫碰撞域),进而可能导致信息被破坏

所以主机中都会有对应的碰撞检测和碰撞避免算法,比如说发现出现了信息碰撞,就休眠一段时间,然后再继续发送信息

反思1:那么如果我们可以做到无限制的发送信息,是不是就可以让局域网瘫痪?

是的,如果能做到无限制发送信息,那么局域网的所有数据发送与接收都会被终止

反思2:为什么只有一个路由器(一个基站的无线网络)可以使用时,一个人使用网速很快,但是一旦多个人使用同一个网络,网速就降低了?
因为多台终端获取网络服务会加剧信息碰撞的概率,所以信息的交互就会变慢

反思3:以太网系统在OS视角可以看成是什么?
以太网可以直接看成是共享资源,碰撞避免实际上是在任意时刻保证只有一台主机进行数据传输,可以看成是锁,而以太网协议栈就可以看成是多个临界区


局域网传输底层过程:

用户A将需要传递的数据依次经过应用层,传输层,网络层,数据链路层,物理层,然后通过局域网进行数据的传递,此时用户B的主机接收到该信息,并依次从物理层传递到应用层

在传输的每一层都需要使用对应的协议,所以传输数据在实际传递的时候会被加工成报文

报文=报头+有效载荷 (报头就是各层的各种协议具体落地结构体),且数据传输的过程中要逐渐追加各层的报头,最后在网络中传输的也是具有报头的报文

类比:报文就是快递,报头就是快递的单号,需要传输的信息就是快递本身

注意:除了应用层外,其他所有层都需要干的事情

1.把报头和有效载荷进行分离(解包)

2.确定需要交给上一层的哪一种协议进行处理(分用)

疑问:为什么要说是TCP/IP协议栈?和栈有什么关系?

这里的栈指的是数据结构里面的栈结构,后进先出

因为在对报文进行加工的过程中,顺序为:数据->应用层报头->....->数据链路层报头

而解包的过程为:数据链路层报头->...->应用层报头->数据

满足后进先出的要求,所以才叫协议栈,主机之间的通信本质就是协议栈之间的通信

1.2.2跨网络传输流程

对于用户1来说,路由器和自己的主机是处于同一局域网的,同理对于用户2来说也是认为处于同一局域网

跨网络传输的时候,使用的传输层协议为TCP,网络层协议是IP协议,所以每台主机又会拥有一个唯一标识的IP地址

IP地址:

IP协议有两个版本,分别是IPV4和IPV6,其中的主流应用版本仍然为IPV4

他们有什么区别?

IPV4占据的是32bit,IPV6占据的是128bit,所以他们能够表示的地址数量是不同的,前者可以表示2^32(约42亿),后者可以表示2^128,所以其实出现IPV6标准的原因就是IP地址不够用了

不过IPV4有临时解决方案,所以IPV6的推广就受阻了

MAC地址和IP地址有什么区别?
在网络传输过程中,源IP地址表示发送源IP地址,目的IP地址表示终端接收者IP地址,他们两者都不变。源MAC地址表示当前路由节点的前一个节点MAC地址,目的MAC地址表示当前路由节点的下一个节点MAC地址,他们两者是会不断改变的。

且目的IP地址决定目的MAC地址


具体传输过程:

对于每一台主机来说,他们都具有路由功能,也就是可以对指定报文进行路径判断,如果属于当前局域网就将他继续发送到局域网的其他主机中,否则就发送给路由器,让路由器进行报文发送

对于发送源需要发送的数据,现在网络层追加源IP地址和目的IP地址,然后交给数据链路层,追加源MAC地址和目的MAC地址

然后目的MAC地址的路由器直接确认接收该数据,进行地址解包,将MAC地址都去除,将具有IP地址信息的数据交给路由器的网络层,并获取最新的目的MAC地址

再将数据交回给数据链路层并通过网卡进行数据后续传输

总结:
1.mac地址只在局域网有效,每经历一个路由器就要对地址进行解包,获得下一组mac地址后再向下交付

2.在网络层向上,所有的地址都是IP地址,在网络层下,地址还包含了局域网地址

2.socket编程

2.1基础知识理解

(1)网络间通信实际上目的是让用户之间通信,而用户间通信又是直接通过app获取信息的,app属于进程

最终,网络间通信就变为了进程间通信,只不过是不同主机上的进程间通信

(2)端口号:是一个2字节的整数,用来表示主机中的某一个进程(独占)

主要作用是告诉操作系统,当前数据要交给哪个进程来处理,一般和IP地址一起作用,用来表明需要将数据传输给哪一个IP地址的主机的哪个端口号进程

注意:

1.通过端口号只能唯一的确定一个进程,但是一个进程可以有多个端口号指向

底层寻找进程其实就是寻找进程的PCB,而PCB是存储在哈希表中的,下标值就是端口号,内容就是PCB结构体指针

2.端口号范围划分:
0~1023:知名端口号,被划分给各种广泛使用的协议,端口号不会改变,且普通进程是无法使用该范围的端口号

1023~65535:普通端口号,可以划分给其他的进程

3.和地址类似:端口号也有源端口号和目的端口号

(3)套接字(socket):是IP地址+port端口号的组合

所以通信的本质就是互联网上两个进程之间在通信

(4)传输层协议TCP和UDP的区别

共同点:都是传输层的协议

区别1:TCP是有连接的,UDP是无连接的

TCP会在通信前进行"三次握手"建立连接,结束通信后"四次挥手"断开连接

UDP直接可以通信,不需要建立连接

区别2:TCP是可靠传输,UDP是不可靠传输

可靠传输指的是TCP会对数据的传输做保证,比如保证数据送达,数据不丢失,不出错等等

不可靠传输指的是UDP不会对发送出去的数据做任何维护,发送出去就和UDP无关了

区别3:TCP面向字节流,UDP面向数据报

TCP面向的字节流被看做无结构的字节流,不具有边界

UDP面向的数据报是独立的有边界的

(5)大端小端:这里指的是硬件的数据存储方案(以字节为单位,不影响字节内顺序)

数据有高权值位和低权值位之分,存储也有高地址和低地址之分

小端存储:将低权值位数据存储到低地址处

大端存储:将高权值位数据存储到高地址处

疑问:如果发送方主机和接收方主机的存储方式不同,怎么办?
对于使用不同大小端存储方式的主机,若不限制他们的发送方式,则他们接收的时候就会出问题.

所以有规定:发送时先发出的是低地址数据,后发出的是高地址数据

于是无论接收和发送方是什么存储方法,他们都可以有固定的数据发送的接收的方法

小端存储就按照小端存储方式解析并接收数据,大端也是同理


socket接口是一层抽象的API,他可以适用于各种底层网络协议,但是底层网络协议的地址格式并不相同

疑问:如何确定需要使用的是哪种结构体?

通过将不同地址类型定义为常量,然后通过传递不同的常量区分属于那种类型

(eg:AF_INET表示IPV4)

通用性:都使用sockaddr*类型,需要具体化使用某一个结构体的时候再类型转化

套接字结构体:保存本端和对端的套接字信息

在bind的时候只有本端套接字信息

对于UDP协议:
在使用recvfrom和sendto函数的时候动态获取指定的对端套接字信息(更加灵活)

eg:邮箱,可以获取各处的邮件

对于TCP协议:

服务端在调用accept函数后才会获取对端套接字信息

客户端会内置对端套接字信息,但是也是在准备connect之前创建出来,然后再connect的时候隐式绑定到sockfd中

eg:电话专线,只能听到指定用户的来电

2.2socket编程:UDP

2.2.1接口

(1)创建socket

参数1:协议族

AF_INET(IPV4),AF_INET6(IPV6),AF_UNIX(本地间进程通信)

参数2:套接字类型

SOCK_STREAM(TCP),SOCK_DGRAM(UDP)

参数3:通常填0

socket()返回一个文件描述符,它是进程访问内核中socket结构体的引用。当通过这个文件描述符进行I/O操作时,内核会将操作路由到对应的socket结构体上,而不是访问磁盘文件

(2)将socket绑定到指定IP地址+端口号

参数1:套接字的fd

参数2:需要绑定的结构体地址

参数3:结构体的大小

返回值:为0表示成功,为-1表示失败

(3)接收远端信息接口:recvfrom

参数1:套接字fd

参数2:缓冲区(用户定义的),用于接收远端信息

参数3:缓冲区大小

参数4:设置为0表示阻塞等待

参数5:输出型参数,由用户定义,然后将远端套接字放入其中

参数6:输入输出型参数,输入时表示存放套接字的结构体大小的变量的指针,输出时表示实际存储的大小的变量的指针

返回值:表示实际读取到的字节数

(4)返回消息给远端:sendto

由于UDP是全双工的,所以sockfd既可以读又可以写(允许同时进行)

参数和recvfrom类似,只是参数2和参数3不是接收的缓冲区,而是存储着发送信息,然后参数5是远端接收的套接字信息

参数6不再是传递指针,而是传递变量本身,因为这里不是输入输出型参数,而是输入型参数,我们不需要获取远端结构体的大小,因为前面获取过了,这里直接输入即可

2.2.2代码编写

1.回显服务代码编写

(1)实现功能:启动服务器端后,客户端可以向服务器端进行数据传输,然后服务器端则将接受到的数据回显给客户端

(2)文件结构:

我们只封装服务端的功能,客户端的直接使用系统提供接口

由于需要将部分信息打印出来,所以需要包含日志头文件,锁头文件

(3)上层业务代码编写

UDPserver.cc

#include"UDPserver.hpp"

int main(int argc,char*argv[])
{
    EnableConsoleLogStrategy();//确定日志输出策略
    if(argc != 2)
    {
        std::cout << "请输入正确格式" << std::endl;
    }
    int port = std::stoi(argv[1]);
    std::unique_ptr<Udpserver> usvr = std::make_unique<Udpserver>(port);
    usvr->Init();
    usvr->Start();
    return 0;
}

服务器流程:确定输出策略->获取端口号->创建Udpserver对象->初始化->启动服务器服务

疑问1:为什么明明需要获取套接字才能进行正常网络通信,这里却只获取了端口号?

答:因为云服务器禁止用户绑定公网ip

即使是可以bind的IP,也不建议使用bind指定IP,而是进行随机IPbind

实现方法1:设置IP的时候设置为宏INADDR_ANY

优化:加上htonl函数,将INADDR_ANY转化为网络传输序列(不加也可以,因为INADDR_ANY全为0)

INADDR_ANY表示可以绑定任意IP地址,所以服务端就可以接收客户端使用不同的服务器端可用IP访问

UDPclient.cc

#include <iostream>
#include <string>
#include <cstring>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
int main(int argc, char *argv[]) // 客户端内置服务端的套接字
{
    if (argc != 3)
    {
        std::cerr << "请输入正确命令行参数" << std::endl;
        exit(1);
    }
    int sockfd = socket(AF_INET, SOCK_DGRAM, 0);//客户端申请套接字
    if (sockfd < 0)
    {
        std::cout << "client socket create failed" << std::endl;
        return 0;
    }
    // 客户端不能显式bind,只能交给os隐式bind随机端口号
    std::string s_ip = argv[1];
    uint16_t s_port = std::stoi(argv[2]);
    struct sockaddr_in server;
    memset(&server, 0, sizeof(server));
    server.sin_family = AF_INET;//设置协议族
    server.sin_port = htons(s_port);//设置端口号
    server.sin_addr.s_addr = inet_addr(s_ip.c_str());//设置ip地址
    //持续申请服务
    while (true)
    {
        std::cout << "please enter: ";
        std::string line;
        std::getline(std::cin, line);
        // 写
        sendto(sockfd, line.c_str(), line.size(), 0, (struct sockaddr *)&server, sizeof(server));
        // 读(一个客户端可以访问多个服务端,这里先不考虑多服务端情况)
        struct sockaddr_in temp;
        socklen_t len = sizeof(temp);
        char buff[1024];
        buff[0] = 0;
        int m = recvfrom(sockfd, buff, sizeof(buff), 0, (struct sockaddr *)&temp, &len);
        if (m > 0)
        {
            buff[m] = 0;
            std::cout <<  buff << std::endl;
        }
    }
    return 0;
}

客户端流程:输入服务器端ip+port->申请套接字->创建并设置目标服务器端结构体->循环进行写入服务器端,从服务器端读的服务请求

疑问1:为什么客户端结构体temp不用进行套接字绑定?

客户端的套接字和结构体不能由用户显式bind,只能交给os进行.这是因为客户端不需要一个固定的端口,服务端因为需要监听多个客户端请求所以才要固定端口

疑问2:为什么设置端口号和ip地址的时候用了两个特殊的接口?他们有什么用?

htons的作用是将大小为两个字节的数据转换为网络传输序列(改为大端排序),inet_addr的作用是将字符串序列转整数,并让其符合网络序列

其实底层ip地址也是有一个结构体,结构体内有四个int指针(IPV4),分别指向ip地址的每一个整数

(4)服务器端封装代码编写

#pragma once
#include <iostream>
#include <memory>
#include<cstdlib>
#include<string>
#include<strings.h>
#include<unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include<arpa/inet.h>
#include<netinet/in.h>
#include"mutex.hpp"
#include"logger.hpp"
static const int gdefaultsockfd = -1;
class Udpserver
{
public:
    Udpserver(uint16_t port)
    :_sockfd(gdefaultsockfd)
    ,_port(port)
    ,_isrunning(false)
    {}
    ~Udpserver() {}
    void Init()
    {
        //创建套接字
        _sockfd = socket(AF_INET,SOCK_DGRAM,0);
        if(_sockfd < 0)
        {
            LOG(loglevel::FATAL) << "create socket failed";
            exit(1);
        }
        LOG(loglevel::INFO) << "create socket success";
        //绑定套接字
        ////填充IP和Port
        struct sockaddr_in local;
        bzero(&local,sizeof(local));
        local.sin_family = AF_INET;
        local.sin_port = htons(_port);//字节序转换,统一为网络传输要求:s表示两字节
        local.sin_addr.s_addr = htonl(INADDR_ANY);//l表示4字节
        //local.sin_addr.s_addr = inet_addr(_ip.c_str());
        //inet_addr:将字符串序列转整数+符合网络序列
        //使用sin_addr.s_addr是因为inet_addr的返回值要对应
        ////bind套接字与结构体
        int n = bind(_sockfd,(struct sockaddr*)&local,sizeof(local));
        if(n < 0)
        {
            LOG(loglevel::FATAL) << "socket bind failed";
            exit(2);
        }
        LOG(loglevel::INFO) << "socket bind success";
    }
    void Start()
    {
        _isrunning = true;
        while(_isrunning)
        {
             char buff[1024];
             buff[0] = 0;
             struct sockaddr_in peer;
             socklen_t len = sizeof(peer);
             int n = recvfrom(_sockfd,buff,sizeof(buff),0,(struct sockaddr*)&peer,&len);
             if(n > 0)//读取成功
             {
                //获取client的套接字
                uint16_t c_port = ntohs(peer.sin_port);
                std::string  c_ip = inet_ntoa(peer.sin_addr);
                 buff[n] = 0;
                 LOG(loglevel::DEBUG) << "[ " << c_ip << ": " << c_port  << " ]# "<< buff; 

                 //信息返回
                 std::string echoinfo = "server echo: ";
                 echoinfo += buff;
                 sendto(_sockfd,echoinfo.c_str(),echoinfo.size(),0,(struct sockaddr*)&peer,len);
             }
        }
        _isrunning = false;
    }
    void Stop()
    {
        _isrunning = false;
    }
private:
    int _sockfd;
    uint16_t _port;
    bool _isrunning;
};

初始化接口:创建出当前服务器的套接字,结构体,并将他们绑定起来

疑问1:为什么这里的ip地址使用了INADDR_ANY?
因为这是服务器端,不能固定绑定IP地址,要可以接收所有可用IP地址,用htonl进行字节序转换是因为这里的数据是常数,只有一字节

启动服务接口:获取远端信息->获取成功后提取client(远端)的套接字信息->根据获取到的信息进行数据回显

疑问1:将网络上获取的套接字转换的接口是什么?
ntohs是将网络序列转为主机序列的接口,inet_ntoa是将整数ip序列转为字符串序列,且将顺序改为主机序列的接口


2.翻译服务代码编写

将上一个回显代码加载进工作区后,将回显功能删除,然后添加了一个dictionary文件,进行翻译工作,服务器端需要添加一个成员变量_cb作为需要执行的任务函数接收

(1)文件结构:

Dict.txt是一个配置文件,里面包含了20个英文单词的翻译信息

(2)需新增与修改的文件

Dictionnary.hpp

#pragma once
#include <iostream>
#include <string>
#include <fstream>
#include <unordered_map>
#include "logger.hpp"
static const std::string sep = ": ";
class Dictionary
{
public:
  void loadconf()
  {
    std::ifstream in(_path);
    if (!in.is_open())
    {
      LOG(loglevel::ERROR) << "配置文件打开失败";
      return;
    }
    // 查找每一行的k_v
    std::string line;
    while (std::getline(in, line))
    {
      auto pos = line.find(sep);
      if (pos == std::string::npos)
      {
        LOG(loglevel::WARNING) << "当前行无目标" << line;
        continue;
      }
      std::string word = line.substr(0, pos);
      std::string value = line.substr(pos + sep.size());
      if (word.empty() || value.empty())
      {
        continue;
      }
      LOG(loglevel::INFO) << "loaddict messa: " << word << "->" << value;
      _dict.insert(std::make_pair(word, value));
    }
    in.close();
  }
  Dictionary(const std::string &path) : _path(path)
  {
    loadconf();
  }
  std::string translate(const std::string &word, const std::string &whoip, uint16_t whoport)
  {
    auto iter = _dict.find(word);
    if(iter == _dict.end())
    {
      LOG(loglevel::WARNING) << "查找失败";
      return "unknow";
    }
    return iter->first + "->" + iter->second;
  }
  ~Dictionary() {}

private:
  std::string _path;
  std::unordered_map<std::string, std::string> _dict;
};

对与loadconf接口,先打开配置文件,然后逐行获取信息,并将信息制成pair插入到_dict哈希表中,最后关闭文件流

疑问1:如何解析配置文件信息?
使用find接口,利用分割符sep将第一个数据位置pos找到,然后使用substr将字符串切割后交给变量

substr的第一个参数是切割起始索引,第二个是切割字符数(默认为结尾)

对于translate接口,查找_dict哈希表中数据,如果有就直接返回数据,没有就打印错误日志

Dictserver.cc

#include"DICTserver.hpp"
#include"Dictionary.hpp"
// std::string translate(const std::string& word,const std::string& whoip, u_int16_t whoport)
// {

//     return "re";
// }
int main(int argc,char*argv[])
{
    if(argc != 2)
    {
        std::cout << "请输入正确格式" << std::endl;
    }
    EnableConsoleLogStrategy();//确定日志输出策略
    int port = std::stoi(argv[1]);
    Dictionary dict("./Dict.txt");
    std::unique_ptr<Dictserver> usvr = std::make_unique<Dictserver>(port,
        [&dict](const std::string& word,const std::string& whoip, uint16_t whoport)
    ->std::string{
            return dict.translate(word,whoip,whoport);
    });
    usvr->Init();
    usvr->Start();
    return 0;
}

我们之所以不将翻译功能和之前的回显功能一样放在Dictionnary.hpp中,是为了进行功能解耦,一个头文件的代码执行一个功能

这里的对象构造直接传递配置文件所处的相对路径,然后传递一个lambda,lambda内部再传递translate接口

疑问1:为什么要使用lambda?
因为要满足返回值和参数的需求来进行传递,而类内的接口会默认包含一个this指针,所以不能直接传递translate,使用lambda可以避免这种情况

3.群聊天代码编写

在翻译服务中,信息流交互逻辑为:

客户端只和服务端交互,服务端和翻译任务与客户端都交互

需求最终是先通过服务端,然后再传给客户端的

而在群聊代码中,由于我们需要实现一人发送的信息全部在线人员都能收到,维护在线人员等任务,所以需要使用多线程管理

服务端直接将从客户端获取的任务信息以及任务处理路由一并交给线程池的任务队列,等待多线程分配执行任务

而这个中间的过程也是一个生产者消费者模型

下面是具体实现方法与代码

文件结构:

包含的自定义头文件

cond.hpp,logger.hpp,mutex.hpp,thread.hpp,threadpool_singol.hpp

修改文件:

chatclient.cc,chatserver.hpp

新增文件:

inetaddr.hpp,Route.hpp,ServerMain.cc


(1)Inetaddr.hpp

#pragma once
#include <iostream>
#include <string>
#include <cstring>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
class Inetaddr
{
public:
    Inetaddr(const struct sockaddr_in &addr)
        : _addr(addr)
    {
        Net2Host();
    }
    Inetaddr(uint16_t port, const std::string &ip = "0.0.0.0")
        : _ip(ip), _port(port)
    {
        Host2Net();
    }
    void Net2Host()
    {
        uint16_t c_port = ntohs(_addr.sin_port);
        std::string c_ip = inet_ntoa(_addr.sin_addr);
    }
    void Host2Net()
    {
        memset(&_addr, 0, sizeof(_addr));
        _addr.sin_family = AF_INET;
        _addr.sin_port = htons(_port);
        _addr.sin_addr.s_addr = inet_addr(_ip.c_str());
    }
    std::string getip()
    {
        return _ip;
    }
    uint16_t getport()
    {
        return _port;
    }
    struct sockaddr *getstruct()
    {
        return (struct sockaddr *)&_addr;
    }
    socklen_t structlen()
    {
        return sizeof(_addr);
    }
    std::string Tostring()
    {
        return _ip + "-" + std::to_string(_port);
    }
    bool operator==(const Inetaddr &addr)
    {
        return (_ip == addr._ip && _port == addr._port);
        // return _ip == addr._ip;//单客户端
    }
    ~Inetaddr() {}

private:
    struct sockaddr_in _addr;
    std::string _ip;
    uint16_t _port;
};

这是一个封装主机转网络,网络转主机的地址转换的类,主要目的是将操作接口化,让主文件可读性更好

地址互转实现方法:
重载不同参数的构造函数,将h2n和n2h的地址转换接口内置进入构造函数,从而在inetaddr对象构造的时候就完成转换(给网络就转主机,给主机就转网络)

疑问:如何提取类内转换完的地址?

直接封装get接口,将类内成员变量的值return给外部变量

(2)chatserver.hpp

#pragma once
#include <iostream>
#include <memory>
#include<cstdlib>
#include<string>
#include<strings.h>
#include<unistd.h>
#include<functional>
#include <sys/types.h>
#include <sys/socket.h>
#include<arpa/inet.h>
#include<netinet/in.h>
#include"mutex.hpp"
#include"logger.hpp"
#include"Inetaddr.hpp"
static const int gdefaultsockfd = -1;
//功能解耦
using callback_t = std::function<void(int sockfd, std::string message, Inetaddr client)>;//不能用引用,因为多线程使用可能会导致变量被释放掉
class Chatserver
{
public:
    Chatserver(uint16_t port,callback_t cb)
    :_sockfd(gdefaultsockfd)
    ,_port(port)
    ,_isrunning(false)
    ,_cb(cb)
    {}
    ~Chatserver() {}
    void Init()
    {
        //创建套接字
        _sockfd = socket(AF_INET,SOCK_DGRAM,0);
        if(_sockfd < 0)
        {
            LOG(loglevel::FATAL) << "create socket failed";
            exit(1);
        }
        LOG(loglevel::INFO) << "create socket success";
        //绑定套接字
        ////填充IP和Port
        // struct sockaddr_in local;
        // bzero(&local,sizeof(local));
        // local.sin_family = AF_INET;
        // local.sin_port = htons(_port);
        // local.sin_addr.s_addr = htonl(INADDR_ANY);

         Inetaddr local(_port);
        ////bind套接字与结构体
        int n = bind(_sockfd,local.getstruct(),local.structlen());
        if(n < 0)
        {
            LOG(loglevel::FATAL) << "socket bind failed";
            exit(2);
        }
        LOG(loglevel::INFO) << "socket bind success";
    }
    void Start()
    {
        _isrunning = true;
        while(_isrunning)
        {
             char buff[1024];
             buff[0] = 0;
             struct sockaddr_in peer;
             socklen_t len = sizeof(peer);
             int n = recvfrom(_sockfd,buff,sizeof(buff),0,(struct sockaddr*)&peer,&len);
             if(n > 0)//读取成功
             {
                buff[n] = 0;
                //获取client的套接字
                 Inetaddr client(peer);
                // uint16_t c_port = ntohs(peer.sin_port);
                // std::string  c_ip = inet_ntoa(peer.sin_addr);

                std::string message = buff;
                _cb(_sockfd,message,client);//将client发送的message发送给所有在线用户
                
                //  sendto(_sockfd,result.c_str(),result.size(),0,(struct sockaddr*)&peer,len);
             }
        }
        _isrunning = false;
    }
    void Stop()
    {
        _isrunning = false;
    }
private:
    int _sockfd;
    uint16_t _port;
    bool _isrunning;
    callback_t _cb;
};

inetaddr优化部分:

 //绑定套接字
        ////填充IP和Port
        // struct sockaddr_in local;
        // bzero(&local,sizeof(local));
        // local.sin_family = AF_INET;
        // local.sin_port = htons(_port);
        // local.sin_addr.s_addr = htonl(INADDR_ANY);

         Inetaddr local(_port);
        ////bind套接字与结构体
        int n = bind(_sockfd,local.getstruct(),local.structlen());

这里将服务器端本地的结构体构建直接接口化了,大大降低了阅读门槛

  //获取client的套接字
                 Inetaddr client(peer);
                // uint16_t c_port = ntohs(peer.sin_port);
                // std::string  c_ip = inet_ntoa(peer.sin_addr);

这里将从远端获取的客户端sockaddr_in结构体(网络地址),提取出来并转换为主机地址,也是接口化了

回调函数部分:
 

using callback_t = std::function<void(int sockfd, std::string message, Inetaddr client)>;
//不能用引用,因为多线程使用可能会导致变量被释放掉

这里是任务信息传递部分,负责将从客户端获取到的信息以及客户端地址,服务器端地址都放到一个函数中,最终要通过Enqueue和路由任务接口一起插入任务队列

(3)Route.hpp

#pragma once
#include <iostream>
#include <vector>
#include <string>
#include "Inetaddr.hpp"

class Route
{
private:
    bool IsExists(const Inetaddr &addr)
    {
        for (auto &e : _online_user)
        {
            if (e == addr)
            {
                return true;
            }
        }
        return false;
    }
    void AddUser(const Inetaddr &addr)
    {
        if (!IsExists(addr)) // 当前传输信息的用户是第一次传输
        {
            _online_user.push_back(addr);
        }
    }
    void DeleteUser(const std::string &message, Inetaddr &client)
    {
        if (message == "QUIT")
        {
            auto iter = _online_user.begin();
            for (; iter != _online_user.end(); iter++)
            {
                if (client == *iter)
                {
                    _online_user.erase(iter);
                    break;
                }
            }
        }
    }
    void SendMessageToAll(int sockfd, std::string &message, Inetaddr &client) // 还要知道是哪个ip发的
    {
        for (auto &user : _online_user)
        {
            std::string info = client.Tostring();
            info += "# ";
            info += message;
            sendto(sockfd, info.c_str(), info.size(), 0, user.getstruct(), user.structlen());
        }
    }

public:
    Route() {}
    // 完成路由任务
    void RouteMessageToAll(int sockfd, std::string &message, Inetaddr &client)
    {
        // 维护用户列表
        AddUser(client);

        // 给所有在线用户传递信息
        SendMessageToAll(sockfd, message, client);
        // 删除用户
        DeleteUser(message, client);
    }
    ~Route() {}

private:
    std::vector<Inetaddr> _online_user;
};

该接口其实就是任务合集,我们可以通过将所有要执行的任务集成在唯一一个开放接口中,然后再编写各个封闭接口(多个任务),最后传递开放接口就可以完成任务路由的传递了

成员变量部分:
使用了Inetaddr作为数组的元素类型,这是因为我们使用IP+port标识唯一一个用户,而Inetaddr又是既能提取网络地址,又能提取主机地址的,具有通用性

接口部分:
唯一的开放接口直接按要求调用需要调用的封闭接口即可,最后使用的也是该接口

(4)ServerMain.cc

#include "Chatserver.hpp"
#include "threadpool_singol.hpp"
#include "Route.hpp"

using task_t = std::function<void()>;
int main(int argc, char *argv[])
{
    if (argc != 2)
    {
        std::cout << "请输入正确格式" << std::endl;
    }
    EnableConsoleLogStrategy(); // 确定日志输出策略
    int port = std::stoi(argv[1]);
    
    //消息转发
    std::unique_ptr<Route> r = std::make_unique<Route>();
    //线程池
    auto t = ThreadPool<task_t>::Getinstance();
    //服务器对象
    std::unique_ptr<Chatserver> usvr = std::make_unique<Chatserver>(port,
        [&t,&r](int sockfd, std::string message, Inetaddr client){
            task_t task = std::bind(&Route::RouteMessageToAll,r.get(),sockfd,message,client);
             t->Enqueue(task);
        });
    usvr->Init();
    usvr->Start();
    return 0;
}

首先创建路由对象(准备好任务接口),然后创建线程池(用于分配任务执行),最后创建chatserver对象,并将任务接口和任务信息bind在一起,最后插入到线程池任务队列

疑问:为什么要bind?
总的来说是为了和任务队列的函数返回值与参数要求保持一致,要求是无返回值与参数,且这里还bind了route对象的指针,这是因为Route的执行接口是类内函数,默认包含this指针,所以为了和该接口契合,这里需要绑定route对象指针

任务执行流:
客户端发送信息->服务端接受信息(获取任务信息),执行回调函数(此时已经结合了路由任务接口)->任务信息与任务执行接口被插入线程池队列->cond处等待唤醒->唤醒后执行线程中的startfunc接口->进入RouteMessageToAll实际执行任务

(5)chatclient.hpp

#include <iostream>
#include <string>
#include <cstring>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <thread>
int sockfd = -2;
std::string s_ip;
uint16_t s_port;
void InitClient(const std::string &s_ip, const int &s_port)
{
    sockfd = socket(AF_INET, SOCK_DGRAM, 0);
}
void recver()
{
    while (true)
    {
        struct sockaddr_in temp;
        socklen_t len = sizeof(temp);
        char buff[1024];
        buff[0] = 0;
        int m = recvfrom(sockfd, buff, sizeof(buff), 0, (struct sockaddr *)&temp, &len);
        if (m > 0)
        {
            buff[m] = 0;
            std::cout << buff << std::endl;
        }
    }
}
void sender()
{
    struct sockaddr_in server;
    // 初始化服务端
    memset(&server, 0, sizeof(server));
    server.sin_family = AF_INET;
    server.sin_port = htons(s_port);
    server.sin_addr.s_addr = inet_addr(s_ip.c_str());
    while (true)
    {
        std::cout << "please enter: ";
        std::string line;
        std::getline(std::cin, line);
        // 写
        sendto(sockfd, line.c_str(), line.size(), 0, (struct sockaddr *)&server, sizeof(server));
    }
}
int main(int argc, char *argv[]) // 客户端内置服务端的套接字
{
    if (argc != 3)
    {
        std::cerr << "请输入正确命令行参数" << std::endl;
        exit(1);
    }
    int sockfd = socket(AF_INET, SOCK_DGRAM, 0); // 客户端申请套接字
    if (sockfd < 0)
    {
        std::cout << "client socket create failed" << std::endl;
        return 0;
    }
    // 客户端不能显式bind,只能交给os隐式bind随机端口号
    s_ip = argv[1];
    s_port = std::stoi(argv[2]);
    InitClient(s_ip, s_port);

    // 创建线程
    std::thread trecv(recver);
    std::thread tsend(sender);

    trecv.join();
    tsend.join();

    return 0;
}

由于原本的client中,客户端的读和写是处于同一线程中执行的,所以读取信息条数是和发送信息条数严格一致的,只有发送了一个信息才能接受一个信息,这是不符合我们的预期的

所以我们需要将读写放在两个线程中执行,这样他们就可以去竞争锁执行任务了,至少比卡在那里要符合预期

然后我们将读取消息和发送消息的执行代码分别放在recver和sender中,作为线程的执行任务

2.3socket编程:TCP

2.3.1接口

1.监听接口:listen

参数1:服务端自己创建的套接字描述符(不负责通信,只用来监听连接请求)

参数2:关系到全连接队列的长度,等于全连接队列长度-1

返回值:0表示成功,-1表示失败

2.获取新链接:accept

参数1:服务端的监听套接字fd

参数2:客户端的套接字结构体地址(输出型参数)

参数3:传递进去时表示预设的结构体大小,传递出来时表示实际接受的客户端结构体大小

返回值:管理对应具体的客户端的服务端sockfd(负责通信)


我们前面创建的fd是服务器端中负责监听的,而由客户端发送请求时监听fd监听到之后会让os调用accept接口,并返回一个通信fd,用来专门和该客户端进行通信

3.发起连接:connect

参数部分和accept一致

返回值:若成功,返回0,若失败返回-1

4.命令解析执行替换:popen

功能:创建管道,子进程,让子进程进程程序替换,执行读取到的命令,将命令执行完成的结果写入管道文件,将管道文件对应的FILE*返回

内部封装了fork,exec,pipe

参数1:需要执行的命令字符串

参数2:读写管道文件的权限

返回值:为nullptr表示创建管道/子进程/执行命令任意步骤出错,为可用FILE指针,那么就是管道文件指针,说明成功

5.字符串序列转换网络地址(线程安全)

参数1:协议族

参数2:待转换的字符串

参数3:输出型参数,接收转换结果的结构体地址

6.网络地址转换字符串序列(线程安全)

参数1:协议族

参数2:装载网络地址的结构体

参数3:用户定义的缓冲区,用于接收转换成功的字符串

参数4:缓冲区的大小

疑问:为什么说上面的两个接口是线程安全?
因为inet_ntoa和inet_aton都是使用系统自带的缓冲区,第二次的调用会覆盖第一次的内容,最终执行出来的内容是最后一次操作的内容

2.3.2代码编写

1.回显服务代码编写

实现功能:实现多客户端并发访问发送信息至服务器,且服务器回显信息给客户端
文件结构:

包含的头文件:
cond.hpp  inetaddr.hpp  logger.hpp  mutex.hpp  thread.hpp threadpool_singol.hpp

新增文件:
Comm.hpp:自定义退出错误码

TCPClient.cc:客户端主程序文件

TCPServer.cc:服务器端主程序文件

TCPEchoServer.hpp:服务器端核心业务头文件

1.Comm.hpp

#pragma once
#include<iostream>
enum
{
  SUCCESS,
  SOCKET_CREATE_ERR,
  SOCKET_BIND_ERR,
  SOCKET_LISTEN_ERR,
  SOCKET_CONNECT_ERR,
  FORK_ERR

};

这里的枚举变量定义都是根据写代码的时候遇到的错误信息来逐渐增加的,这里直接给出完整版本

2.TCPClient.cc

#include <iostream>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include "Comm.hpp"
#include "Inetaddr.hpp"
int main(int argc, char *argv[])
{
    if (argc != 3)
    {
        std::cerr << "请输入正确命令行参数" << std::endl;
        exit(1);
    }
    // 客户端中的服务端套接字信息一般内置(程序员设定)
    std::string serverip = argv[1];
    int serverport = std::stoi(argv[2]);
    // 创建socket
    int sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (sockfd < 0)
    {
        std::cerr << "create sockfd falied" << std::endl;
        exit(SOCKET_CREATE_ERR);
    }
    // 建立连接
    Inetaddr server(serverport, serverip);
    if (!connect(sockfd, (struct sockaddr *)&server, server.structlen()))
    {
        std::cerr << "connect server failed" << std::endl;
        exit(SOCKET_CONNECT_ERR);
    }
    std::cout << "connect" << server.Tostring() << "success" << std::endl;
    // 读写信息
    while (true)
    {
        std::cout << "please enter: ";
        std::string message;
        std::getline(std::cin, message);
        // 写数据
        ssize_t n = write(sockfd, message.c_str(), message.size());
        if (n >= 0)
        {
            char buff[1024];
            ssize_t m = read(sockfd, buff, sizeof(buff) - 1);
            if (m > 0)
            {
                buff[m] = 0;
                std::cout << buff << std::endl;
            }
        }
    }
    return 0;
}

TCP客户端通信准备过程:
获取内置的服务器端套接字(IP+port)

->申请创建客户端sockfd

->使用connect接口建立连接

注意:
1.与服务器端不同:客户端的sockfd不建议显示bind本地套接字结构体,因为客户端的端口号一般是动态分配的,不是内置固定的

2.客户端的套接字结构体一般是在connect的时候完成和sockfd的bind(此时结构体同时包含本端和远端的套接字信息)

交互过程:

发送信息:读取显示器文件中用户输入的信息,并用write发送

读取信息:将信息通过read读取到预留的存储数组中
由于TCP是面向字节流的,所以这里建立连接之后使用的是文件读写方式进行发送和读取信息

注意:
1.读取的时候需要预留一个位置,因为读取的都是字符,预留一位添加\0,防止出现乱码

3.TCPServer.cc:服务器端主程序文件

#include"TCPEchoServer.hpp"
#include<memory>
int main(int argc, char* argv[])
{
     if (argc != 2)
    {
        std::cerr << "请输入正确命令行参数" << std::endl;
        exit(1);
    }
    EnableConsoleLogStrategy();
    std::unique_ptr<TcpEchoServer> tsvr = std::make_unique<TcpEchoServer>();
    tsvr->Init();
    tsvr->Start();
    return 0;
}

服务器端只需要正常初始化并启动即可,主要逻辑在.hpp文件中

首先从显示器中读取程序员设定的服务器端口号,然后创建服务器对象并运行初始化接口与运行接口

4.TCPEchoServer.hpp

#pragma once
#include <iostream>
#include <string>
#include <cstring>
#include <unistd.h>
#include <pthread.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/wait.h>
#include "Comm.hpp"
#include "Inetaddr.hpp"
#include "logger.hpp"
#include "threadpool_singol.hpp"
const static int defaultsockfd = -1;
const static int gbacklog = 8;
const static int gport = 8080;
using task_t = std::function<void()>;
class TcpEchoServer
{
private:
//长连接,不适用线程池技术
    void HandlerIO(int sockfd, Inetaddr client) // 单进程版本:直接使用
    {
        while (true)
        {
            char buff[1024];
            ssize_t n = read(sockfd, buff, sizeof(buff) - 1);
            if (n > 0) // 读取成功,准备回显
            {
                buff[n] = 0;
                std::string info = "server echo# ";
                info += buff;
                write(sockfd, &info, info.size());
            }
            else if (n == 0) // 客户端退出
            {
                LOG(loglevel::INFO) << "client: " << client.Tostring() << "quit";
                break;
            }
            else
            {
                LOG(loglevel::FATAL) << "read client " << client.Tostring() << "error, sockfd: " << sockfd;
                break;
            }
        }

        close(sockfd); //
    }

public:
    TcpEchoServer(uint16_t port = gport, int sockfd = defaultsockfd)
        : _listen_sockfd(sockfd), _port(port)
    {
    }
    ~TcpEchoServer() {}
    void Init()
    {
        // 申请创建套接字
        _listen_sockfd = socket(AF_INET, SOCK_STREAM, 0);
        if (_listen_sockfd < 0)
        {
            LOG(loglevel::FATAL) << "socket create fail";
            exit(SOCKET_CREATE_ERR);
        }
        LOG(loglevel::INFO) << "socket create success: " << _listen_sockfd;
        // 绑定套接字信息结构体
        Inetaddr local(_port);
        if (bind(_listen_sockfd, (struct sockaddr *)&local, sizeof(local) != 0))
        {
            LOG(loglevel::FATAL) << "socket bind failed";
            exit(SOCKET_BIND_ERR);
        }
        LOG(loglevel::INFO) << "socket bind success" << _listen_sockfd;
        // 设置套接字监听
        if (listen(_listen_sockfd, gbacklog) != 0)
        {
            LOG(loglevel::FATAL) << "listen failed";
            exit(SOCKET_LISTEN_ERR);
        }
        LOG(loglevel::INFO) << "listen success";
    }
    class ThreadData // 用于解决static函数无法使用类内接口问题
    {
    public:
        ThreadData(int sockfd, TcpEchoServer *ptr, Inetaddr addr)
            : _sockfd(sockfd), _self(ptr), _addr(addr)
        {
        }
        // private:
        int _sockfd;
        TcpEchoServer *_self;
        Inetaddr _addr;
    };
    static void *Routine(void *args) // 线程执行函数
    {
        ThreadData *td = static_cast<ThreadData *>(args);
        pthread_detach(pthread_self());
        td->_self->HandlerIO(td->_sockfd, td->_addr);
    }
    void Start()
    {
        while (true)
        {
            struct sockaddr_in peer;
            socklen_t len = sizeof(peer);
            int sockfd = accept(_listen_sockfd, (struct sockaddr *)&peer, &len);
            if (sockfd < 0)
            {
                LOG(loglevel::WARNING) << "accept failed";
                continue;
            }
            Inetaddr clientaddr(peer);
            LOG(loglevel::INFO) << "获取新链接成功,sockfd:" << sockfd << "client addr: "
                                << clientaddr.Tostring();
            //线程池
           ThreadPool<task_t>::Getinstance()->Enqueue([this,sockfd,clientaddr](){
               this->HandlerIO(sockfd,clientaddr);//这里clientaddr是常量所以要将handlerio的参数的引用去除(这里捕捉不同引用也是因为这里是临时变量)
           });



            // 多进程和多线程会存在问题:
            // 1.效率问题,连接成功才创建
            // 2.执行流无上限,可能会超载崩溃
            /////////////////////////////////////////
            // 多线程版本
            // pthread_t tid;
            // ThreadData *td = new ThreadData(sockfd, this, clientaddr);
            // pthread_create(&tid, nullptr, Routine, (void *)this);
            //////////////////////////////
            // 多进程版本
            //  pid_t id = fork();
            //  if (id < 0)
            //  {
            //      LOG(loglevel::FATAL) << "资源不足,子进程创建失败";
            //      exit(FORK_ERR);
            //  }
            //  else if (id == 0)
            //  {
            //      close(_listen_sockfd);
            //      if(fork())//父进程直接返回了
            //      {
            //          exit(SUCCESS);
            //      }
            //      HandlerIO(sockfd,clientaddr);//孙子进程继续运行,且由于父进程退出,被os托管
            //  }
            //  else
            //  {
            //      close(sockfd);//规避fd泄漏
            //      pid_t rid = waitpid(id,nullptr,0);//不会被孙子进程阻塞
            //  }
            ///////////////////////////////////////////////
        }
    }

private:
    int _listen_sockfd;
    uint16_t _port;
};

初始化接口:Init()

申请创建listen_sockfd->bind包含本地信息的套接字结构体->添加监听

注意:
1.初始化时候用到的sockfd是监听sockfd,他不负责进行与客户端的实际通信,他只负责监听客户端的连接请求,请求连接成功就会生成一个新的sockfd进行与该客户端的实际通信

通信处理接口:Handlerio()

由于是服务器端,所以先读(read),读取成功就将数据写(write)回给客户端

读取失败有两种情况:但是都是直接退出函数
1.客户端退出:此时read返回值为0

2.客户端没退出,但是读取异常:返回值小于0

启动接口:Start()

(死循环进行):accept客户端连接请求->处理当前客户端的通信任务

分为四个版本:
版本一:单进程版本

直接使用Handlerio接口

缺点:tcp中如果是单进程运行,同一时间只有一个客户端可以获取服务,因为获取服务的前提是服务端和客户端建立连接

版本二:多进程版本

 //多进程版本
             pid_t id = fork();
             if (id < 0)
             {
                 LOG(loglevel::FATAL) << "资源不足,子进程创建失败";
                 exit(FORK_ERR);
             }
             else if (id == 0)
             {
                 close(_listen_sockfd);
                 if(fork())//父进程直接返回了
                 {
                     exit(SUCCESS);
                 }
                 HandlerIO(sockfd,clientaddr);//孙子进程继续运行,且由于父进程退出,被os托管
             }
             else
             {
                 close(sockfd);//规避fd泄漏
                 pid_t rid = waitpid(id,nullptr,0);//不会被孙子进程阻塞
             }

核心思想是让子进程进行具体任务执行,但是由于父进程还需要等待子进程退出才能退出,如果直接让父进程等子进程执行完,就又变成了串行执行,多进程就没有意义了

小设计:
在原来的子进程执行流中再进行一次fork,然后若创建成功,当前进程就返回,然后当前进程的父进程就可以等待成功,继续进行下一次循环,执行下一个客户端请求的任务

而真正执行Handlerio的孙子进程则由于父进程退出,变为os托管的孤儿进程

综上,当前任务执行的等待就从爷爷进程变为os了

版本三:多线程版本

 class ThreadData // 用于解决static函数无法使用类内接口问题
    {
    public:
        ThreadData(int sockfd, TcpEchoServer *ptr, Inetaddr addr)
            : _sockfd(sockfd), _self(ptr), _addr(addr)
        {
        }
        // private:
        int _sockfd;
        TcpEchoServer *_self;
        Inetaddr _addr;
    };
    static void *Routine(void *args) // 线程执行函数
    {
        ThreadData *td = static_cast<ThreadData *>(args);
        pthread_detach(pthread_self());
        td->_self->HandlerIO(td->_sockfd, td->_addr);
    }
pthread_t tid;
ThreadData *td = new ThreadData(sockfd, this, clientaddr);
pthread_create(&tid, nullptr, Routine, (void *)td);

通过创建多线程来让不同的执行流分别执行对应客户端的请求,但是为了让线程不用等待,直接datach分离线程

疑问:使用HandlerIO接口需要知道sockfd和clientaddr,static类型接口HandlerIO无法使用Start接口内部的变量值以及this指针,怎么办?

我们可以定义一个内部类,专门负责获取sockfd,this指针,clientaddr

在start内部创建ThreadData对象并传递需要的值,然后作为参数传递给线程执行接口,在Routine中将args转换为ThreadData类型指针,然后就可以访问对象的成员变量,以此获取需要的值

多线程/多进程缺陷:

1.效率问题,连接成功才创建

2.执行流无上限,可能会超载崩溃

版本四:线程池

     //线程池
ThreadPool<task_t>::Getinstance()->Enqueue([this,sockfd,clientaddr](){
   this->HandlerIO(sockfd,clientaddr);
           });

这里通过Getinstance获取单例模式的线程池对象,然后调用Enqueue就爱那个任务插入队列中

注意:
clientaddr是常量所以要将handlerio的参数的引用去除(这里捕捉不同引用也是因为这里是临时变量)

2.远程执行命令代码编写

实现功能:通过客户端输入命令,让服务器端执行命令,并将执行结果返回给客户端

文件结构:

新增与修改文件:
1.Command.hpp

2.Inetaddr.hpp

3.Main.cc

(1)Command.hpp

#pragma once
#include <cstdio>
#include <iostream>
#include <string>
#include<vector>
class Command
{
    private:
    bool Issafe(const std::string &cmd)
    {
         for(auto &e : _command_white_list)
         {
            if(e == cmd)
            {
                return true;
            }
         }
         return false;
    }
public:
    Command() 
    {
        _command_white_list.push_back("ls");
        _command_white_list.push_back("touch test.txt");
        _command_white_list.push_back("tree");
        _command_white_list.push_back("rm test.txt");
    }
    std::string Exec(const std::string &cmd)
    {
        if(!Issafe(cmd))
        {
          return "false";
        }
        std::string result;
        FILE *fp = popen(cmd.c_str(), "r");
        if (fp == NULL)
        {
            result = cmd + "exec error";
        }
        else
        {
            char buff[1024];
            if(fgets(buff,sizeof(buff),fp) != nullptr)
            {
                   result += buff;
            }
            pclose(fp);
        }
        return result;
    }
    ~Command() {}

private:
std::vector<std::string> _command_white_list;
};

该类的作用是对客户端需要执行的命令进行执行,完成执行的主体功能

具体来说是通过Exec接口进行命令读取与解析,然后执行命令并将结果放置在新打开的管道文件当中,最终将执行结果读取到指定缓冲区中,并最终赋值给result结果字符串

疑问:如果客户端输入的命令是非法命令,或者不支持命令怎么办?
由于我们只是简单实现远程命令执行,所以我们可以限制用户能执行的命令,也就是添加一个命令白名单,只有白名单(字符串数组)上的命令可以执行

(2)Inetaddr.hpp

#pragma once
#include <iostream>
#include <string>
#include <cstring>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
class Inetaddr
{
public:
    Inetaddr(const struct sockaddr_in &addr)
        : _addr(addr)
    {
        Net2Host();
    }
    Inetaddr(uint16_t port, const std::string &ip = "0.0.0.0")
        : _ip(ip), _port(port)
    {
        Host2Net();
    }
    void Net2Host()
    {
        uint16_t c_port = ntohs(_addr.sin_port);
        // std::string c_ip = inet_ntoa(_addr.sin_addr);
        char buff[1024];
        inet_ntop(AF_INET,&(_addr.sin_addr),buff,sizeof(buff));
        _ip = buff;
    }
    void Host2Net()
    {
        memset(&_addr, 0, sizeof(_addr));
        _addr.sin_family = AF_INET;
        _addr.sin_port = htons(_port);
        // _addr.sin_addr.s_addr = inet_addr(_ip.c_str());
        inet_pton(AF_INET,_ip.c_str(),&(_addr.sin_addr));//字符串转网络序列
    }
    std::string getip()
    {
        return _ip;
    }
    uint16_t getport()
    {
        return _port;
    }
    struct sockaddr *getstruct()
    {
        return (struct sockaddr *)&_addr;
    }
    socklen_t structlen()
    {
        return sizeof(_addr);
    }
    std::string Tostring()
    {
        return _ip + "-" + std::to_string(_port);
    }
    bool operator==(const Inetaddr &addr)
    {
        return (_ip == addr._ip && _port == addr._port);
        // return _ip == addr._ip;//单客户端
    }
    ~Inetaddr() {}

private:
    struct sockaddr_in _addr;
    std::string _ip;
    uint16_t _port;
};

这里的主要修改点是将线程不安全的inet_ntoa和inet_aton接口替换掉,替换为inet_ntop和inet_pton

虽然替换的接口使用起来有点麻烦,但是可以避免出现内容覆盖导致的错误问题

3.Main.cc

#include"CommandServer.hpp"
#include"Command.hpp"
#include<memory>
//使用对象来调用函数完成任务
// std::string CommandExec(const std::string& commandstr)
// {
//   return "lll";
// }
int main(int argc, char* argv[])
{
     if (argc != 2)
    {
        std::cerr << "请输入正确命令行参数" << std::endl;
        exit(1);
    }
    EnableConsoleLogStrategy();
    int serverport = std::stoi(argv[1]);
    Command cmdobj;
    std::unique_ptr<CommandServer> tsvr = std::make_unique<CommandServer>(
        [&cmdobj](const std::string &cmd)->std::string{
                 return cmdobj.Exec(cmd);
        },serverport);
    tsvr->Init();
    tsvr->Start();
    return 0;
}

传递的线程执行接口就是进行远程命令执行的Exec,不过为了避免参数不匹配问题出现,需要使用lambda对函数再次封装

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐