18fde01fee5e4278981004762ce48cc4.png

✨✨ 欢迎大家来到小伞的大讲堂✨✨

🎈🎈养成好习惯,先赞后看哦~🎈🎈

所属专栏:LInux
小伞的主页:xiaosan_blog

gitee:许星让 (xu-xingrang) - Gitee.com

制作不易!点个赞吧!!谢谢喵!!

0.本节目录gitee链接

完整代码:

lesson40 · 许星让/linux gcc - 码云 - 开源中国

0.1 关于Tcp协议

Tcp全称“传输控制协议(Transmission Control Protocol)”,是当今互联网使用最广泛的传输层协议,因为它基于通信时保证可靠性,并且对于高效传输也有一定策略,是目前应用层底层使用的协议中非常常见和重要的一种网络协议

我们来认识下“传输控制协议”的“控制”二字含义:

  • 之前实现的网络版计算器,用的是http/https,底层就是Tcp,在通信双方内部都有着发送缓冲区和接收缓冲区,我们调用write,read,recv,send等操作其实不是把数据直接发送到网络中,而是把数据拷贝到操作系统的内核缓冲区中
  • 至于这些数据什么时候发,发多少,出错了怎么办?这个完全由Tcp协议自主决定,所以上面那些函数本质叫做“拷贝函数”
  • 这和我们文件操作一样,用write通过文件描述符写入时,都是先把文件数据拷贝一份到某个缓冲区里,然后数据从缓冲区再刷新到磁盘文件上,这部分操作完全由操作系统和磁盘驱动决定。这么看来,只要把磁盘设备换成网卡,让操作系统把数据拷贝到网卡上,就可以完成数据远程发送了。
  • 而数据发送,本质也就是把数据从我们的发送缓冲区拷贝到对方的接收缓冲区,所以网络发送“本质也是拷贝”。但是网络传输距离很长,所以可能会出错,TCP就是为了应对长距离传输出错而做的策略
  • 因为Tcp既能接收也能发送,所以双方的地位是对等的,服务器构建响应,其实也就是把响应封装好,然后通过应用层的各种接口把响应拷贝到TCP发送缓冲区,而客户端作也有接收缓冲区和发送缓冲区,所以网络发送也可以看作是一种“拷贝”,你给我拷贝的同时我可以给你拷,你不给我拷贝的时候我也可以给你拷贝,这个过程就叫做“全双工”。
  • 所以,文件描述符和套接字一样是可写可读的,因为双方的发送和接收缓冲区是独立的,不会相互影响

背景:缓冲区也是内存空间,操作系统为了管理内存,把整个内存在逻辑上分成很多的4KB的空间,所以内存内部有大量的内存块,为了管理内存块就有很多struct_page这样的结构。所以发送和接收缓冲区是由很多个4KB的内存块和struct_page构成的,然后先描述,再组织。所以打开一个网络套接字就是打开了一个文件描述符,而一个打开的文件也一定有负责管理它的 struct_file 对象,而这个对象里面就包含有个指针指向磁盘,而有了网络之后,struct_file 的这个指针就指向网卡设备,此时就能上层不变,下层就能直接切换成网络

所以“传输控制协议”的“控制”,应用层把数据拷贝到发送缓冲区里,什么时候发,发多少,出错了怎么办,其实本质也就是在控制如何发送的问题,这些工作由Tcp协议自主决定,所以我们把TCP叫做“传输控制协议”。 
 

0.2 Tcp报头字段解析

三部分,标准报头(前20字节),选项(暂时忽略),有效载荷(要传输的数据)。前两个字段16位源端口号和16位目的端口号和UDP一样,能够将报文数据交付给上层的某个协议, 

Tcp报头在内核代码中就是一个位段类型,给数据封装Tcp报头时,实际上就是用这个位段类型定义一个变量,然后往该变量中填充Tcp报头的各个属性字段,这个步骤和我们之前填充 sockaddr结构体的步骤类似

可以看到,Tcp协议的报头字段相比Udp多了很多,因为Tcp要保证可靠性,所以Tcp会在底层做更多的动作,下面我们来详细解释下各字段的作用 

后续会重谈Tcp机制!!!

1.TCP socket API 详解

如图:客户端与服务端通过类似于文件描述符的进行读写

  • socket()打开一个网络通讯端口,如果成功的话,就像open()一样返回一个文件描述符;
  • 应用程序可以像读写文件一样用read/write在网络上收发数据;
  • 如果 socket()调用出错则返回-1
  • 对于 IPv4,family 参数指定为 AF_INET;(即将接收地址设置为0,任何连接客户端,都接收)
  • 对于 TCP 协议,type 参数指定为 SOCK_STREAM,表示面向流的传输协议
  • protocol参数的介绍从略,指定为0即可

1.2 bind 

  • 服务器程序所监听的网络地址和端口号通常是固定不变的,客户端程序得知服务器程序的地址和端口号后就可以向服务器发起连接;服务器需要调用bind绑定一个固定的网络地址和端口号;
  • bind()成功返回 0,失败返回-1。
  • bind()的作用是将参数sockfd和myaddr绑定在一起,使sockfd这个用于网络通讯的文件描述符监听myaddr所描述的地址和端口号;
  • 前面讲过,structsockaddr*是一个通用指针类型,myaddr参数实际上可以接受多种协议的sockaddr结构体,而它们的长度各不相同,所以需要第三个参数addrlen指定结构体的长度;

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

  1. 将整个结构体清零;
  2. 设置地址类型为AF_INET;
  3. 网络地址为INADDR_ANY,这个宏表示本地的任意IP地址,因为服务器可能有多个网卡,每个网卡也可能绑定多个IP 地址,这样设置可以在所有的 IP地址上监听,直到与某个客户端建立了连接时才确定下来到底用哪个IP 地址;
  4. 端口号为SERV_PORT,我们定义为9999;

1.4 listen

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

1.5 accept

  • 三次握手完成后,服务器调用accept()接受连接
  • 如果服务器调用 accept()时还没有客户端的连接请求,就阻塞等待直到有客户端连接上来;
  • addr是一个传出参数,accept()返回时传出客户端的地址和端口号;
  • 如果给 addr 参数传 NULL,表示不关心客户端的地址;
  • addrlen参数是一个传入传出参数(value-result argument),传入的是调用者提供的,缓冲区addr的长度以避免缓冲区溢出问题,传出的是客户端地址结构体的实际长度(有可能没有占满调用者提供的缓冲区);

1.6 connect

  • 客户端需要调用connect()连接服务器;
  • connect 和 bind 的参数形式一致,区别在于 bind 的参数是自己的地址,而connect 的参数是对方的地址;
  • connect()成功返回0,出错返回-1;

2. V1 - Echo Server

2.1 nocopy.hpp

由于socket是禁止拷贝构造的,所以我们可以通过继承解决这个问题

#pragma once
#include <iostream>

class nocopy
{
public:
    nocopy(){}
    nocopy(const nocopy &) = delete;
    const nocopy& operator = (const nocopy &) = delete;
    ~nocopy(){}
};

2.1 TcpServer.hpp

#pragma once
#include "Common.hpp"
#include "Log.hpp"
#include "InetAddr.hpp"
#include <sys/wait.h>
#include <signal.h>
#include "ThreadPool.hpp"

// 服务器往往是禁止拷贝的
using namespace LogModule;
using namespace ThreadPoolModule;

// using task_t = std::function<void()>;
using func_t = std::function<std::string(const std::string &, InetAddr &word)>;

const static int defaultsockfd = -1;
const static int backlog = 10;

class TcpServer : public Nocopy
{
public:
    TcpServer(uint16_t port, func_t func)
        : _port(port),
          _func(func),
          _listensockfd(defaultsockfd),
          _isrunning(false)
    {
    }
    void Init()
    {
        signal(SIGCHLD, SIG_IGN); // 子进程退出发送SIGCHLD信号,用SIG_IGN信号忽略子进程信号
        // 1. 创建套接字文件
        _listensockfd = socket(AF_INET, SOCK_STREAM, 0);
        if (_listensockfd < 0)
        {
            LOG(LogLevel::FATAL) << "socket error!";
            exit(SOCKET_ERR);
        }
        LOG(LogLevel::INFO) << "socket success: " << _listensockfd;
        // 2. bind 绑定端口号
        InetAddr local(_port);
        int n = bind(_listensockfd, local.NetAddrPtr(), local.NetAddrLen());
        if (n < 0)
        {
            LOG(LogLevel::FATAL) << "bind error";
            exit(BIND_ERR);
        }
        LOG(LogLevel::INFO) << "bind success: " << _listensockfd;
        // 设置_sockfd为监听状态
        n = listen(_listensockfd, backlog);
        if (n < 0)
        {
            LOG(LogLevel::FATAL) << "listen error";
            exit(LISTEN_ERR);
        }
        LOG(LogLevel::INFO) << "listen success: " << _listensockfd;
    }

    void Service(int sockfd, InetAddr &peer)
    {
        char buffer[1024];
        while (true)
        {
            ssize_t n = read(sockfd, buffer, sizeof(buffer) - 1);
            if (n > 0)
            {
                buffer[n] = 0; // 设置为C风格字符串, n<= sizeof(buffer)-1
                LOG(LogLevel::DEBUG) << peer.StringAddr() << " say#" << buffer;
                std::string echo_string = _func(buffer, peer);
                // std::string echo_string = "echo# ";
                // echo_string += buffer;
                write(sockfd, echo_string.c_str(), echo_string.size());
            }
            else if (n == 0)
            {
                LOG(LogLevel::DEBUG) << peer.StringAddr() << " 退出了...";
                close(sockfd);
                break;
            }
            else
            {
                LOG(LogLevel::DEBUG) << peer.StringAddr() << " 异常...";
                close(sockfd);
                break;
            }
        }
    }

    class ThreadData
    {
    public:
        ThreadData(int sockfd, InetAddr addr, TcpServer *tsvr)
            : _sockfd(sockfd),
              _addr(addr),
              _tsvr(tsvr)
        {
        }

    public:
        int _sockfd;
        InetAddr _addr;
        TcpServer *_tsvr;
    };

    static void *Routine(void *args)
    {
        pthread_detach(pthread_self()); // 分离线程
        ThreadData *td = static_cast<ThreadData *>(args);
        td->_tsvr->Service(td->_sockfd, td->_addr);
        delete td;
        return nullptr;
    }

    void Run()
    {
        _isrunning = true;
        while (_isrunning)
        {
            // a.获取状态
            struct sockaddr_in peer;
            socklen_t len = sizeof(sockaddr_in);
            // sockfd 提供服务,_listensockfd 提供链接
            int sockfd = accept(_listensockfd, CONV(peer), &len);
            if (sockfd < 0)
            {
                LOG(LogLevel::WARNING) << "accept error";
                continue;
            }
            InetAddr addr(peer);
            LOG(LogLevel::INFO) << "accept sucess, peer addr : " << addr.StringAddr();

            // 多线程适合长服务
            ThreadData *td = new ThreadData(sockfd, addr, this);
            pthread_t tid;
            pthread_create(&tid, nullptr, Routine, td);

            // 0.单进程版本
            // Service(sockfd, addr);
            // 1.多进程版本
            // pid_t id = fork();
            // if (id < 0)
            // {
            //     LOG(LogLevel::FATAL) << "fork error";
            //     exit(FORK_ERR);
            // }
            // else if (id == 0)
            // {
            //     // 子进程
            //     if (fork() > 0)
            //         exit(OK);
            //     //孙子进程
            //     //当子进程返回,孙子进程为孤儿进程,父进程变为系统bash,系统自动回收进程
            //     close(_listensockfd); // 关闭不相关的管道
            //     Service(sockfd, addr);
            //     exit(OK);
            // }
            // else
            // {
            //     // 父进程
            //     close(sockfd);
            //     pid_t rid = waitpid(id, nullptr, 0); // 阻塞等待
            // }
            // 2.多线程
            // ThreadData *td = new ThreadData(sockfd, addr, this);
            // pthread_t tid;
            // pthread_create(&tid, nullptr, Routine, td);
            // //pthread_join()线程等待,也会导致线程串行,使用pthread_detach线程分离,无需等待;

            // version3:线程池版本,线程池一般比较适合处理短服务
            // 将新链接和客户端构建一个新的任务,push线程池中
            // ThreadPool<task_t>::GetInstance()->Enqueue([this,sockfd,&addr](){
            //     this->Service(sockfd,addr);
            // });
        }
        _isrunning = false;
    }
    ~TcpServer() {}

private:
    int _port;
    int _listensockfd; // 监听socket
    bool _isrunning;
    func_t _func; // 设置回调处理
};

2.2 TcpServer.cc

#include "TcpServer.hpp"
#include "Dict.hpp"
#include "Command.hpp"
void Usage(std::string proc)
{
    std::cerr << "Usage" << proc << " port" << std::endl;
}

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

    uint16_t port = std::stoi(argv[1]);

    Enable_Console_Log_Strategy();

    // Dict d;
    // d.LoadDict();
    Command cmd;
    //命令行xshell
    func_t ret = std::bind(&Command::Execute,&cmd,std::placeholders::_1,std::placeholders::_2);
    std::unique_ptr<TcpServer> tsvr = std::make_unique<TcpServer>(port,ret);
    
    // std::unique_ptr<TcpServer> tsvr = std::make_unique<TcpServer>(port, [&cmd](const std::string &command, InetAddr &addr)
    // { return cmd.Execute(command, addr); });
    
    //翻译
    // std::unique_ptr<TcpServer> tsvr = std::make_unique<TcpServer>(port);
    // std::unique_ptr<TcpServer> tsvr = std::make_unique<TcpServer>(port, [&d](const std::string &word, InetAddr &addr)
    // { return d.Translate(word, addr); });

    tsvr->Init();
    tsvr->Run();

    return 0;
}

2.3 TcpClient

#include <iostream>
#include "Common.hpp"
#include "InetAddr.hpp"
void Usage(std::string proc)
{
     std::cerr << "Usage: " << proc << " server_ip server_port" << std::endl;
}

int main(int argc, char *argv[])
{
    if (argc != 3)
    {
        Usage(argv[0]);
        exit(USAGE_ERR);
    }
    std::string serverip = argv[1];
    uint16_t serverport = std::stoi(argv[2]);

    // 1.创建套接字
    int sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (sockfd < 0)
    {
        std::cerr << "sockfd error" << std::endl;
        exit(SOCKET_ERR);
    }
    // 客户端需要bind,但,显示的bind使不需要的
    // 不需要listen,accep,
    // 直接向目标发起连接请求
    InetAddr serveraddr(serverip, serverport);
    int n = connect(sockfd, serveraddr.NetAddrPtr(), serveraddr.NetAddrLen());
    //连接成功,系统自动bind
    if (n < 0)
    {
        perror("error:");
        std::cerr << "connect error" << std::endl;
        exit(CONNECT_ERR);
    }

    while (true)
    {
        std::string line;
        std::cout << "Please Enter@ ";
        getline(std::cin, line);
        write(sockfd, line.c_str(), line.size());
        char buffer[1024];
        ssize_t size = read(sockfd, buffer, sizeof(buffer));
        if (size > 0)
        {
            buffer[size] = 0;
            std::cout << "server echo# " << buffer << std::endl;
        }
    }

    close(sockfd);
    return 0;
}

2.4 dictionary(翻译功能)

2.4.1 dictionary.txt

(确保翻译功能的格式)

apple: 苹果
banana: 香蕉
cat: 猫
dog: 狗
book: 书
pen: 笔
happy: 快乐的
sad: 悲伤的
hello: 你好
run: 跑
jump: 跳
teacher: 老师
student: 学生
car: 汽车
bus: 公交车
love: 爱
hate: 恨
hello: 你好
goodbye: 再见
summer: 夏天
winter: 冬天
2.4.2 Dict.hpp(翻译功能的实现)
#pragma once

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

using namespace LogModule;

const std::string defaultdict = "./dictionary.txt"; // 当前目录
const std::string sep = ": ";                       // apple: 苹果 :为分隔符

class Dict
{
public:
    Dict(const std::string &path = defaultdict) : _dict_path(path)
    {
    }
    bool LoadDict()
    {
        std::ifstream in(_dict_path);
        if (!in.is_open())
        {
            LOG(LogLevel::DEBUG) << "打开字典" << _dict_path << "失败";
            return false;
        }
        std::string line;
        while (std::getline(in, line))
        {
            auto pos = line.find(sep);
            if (pos == std::string::npos) // npos == -1, 表示没有找到
            {
                LOG(LogLevel::WARNING) << "解析: " << line << " 失败";
                continue; // 此时跳过,等待下次的查询
            }
            std::string english = line.substr(0, pos);
            std::string chinese = line.substr(pos + sep.size());
            if (english.empty() || chinese.empty())
            {
                LOG(LogLevel::WARNING) << "没有有效内容: " << line;
                continue;
            }
            _dict.insert(std::make_pair(english, chinese));
            LOG(LogLevel::DEBUG) << "加载: " << line;
        }
        in.close();
        return true;
    }

    std::string Translate(const std::string &word, InetAddr &client)
    {
        auto iter = _dict.find(word);
        if (iter == _dict.end()) // 如果没有找到会返回该字符串最后一位
        {
            LOG(LogLevel::DEBUG) << "进入到了翻译模块, [" << client.Ip() << " : " << client.Port() << "]# " << word << "->None";
            return "None";
        }
        LOG(LogLevel::DEBUG) << "进入到了翻译模块, [" << client.Ip() << " : " << client.Port() << "]# " << word << "->" << iter->second;
        return iter->second;
    }

private:
    std::string _dict_path;                             // 翻译文件// 路径+文件名
    std::unordered_map<std::string, std::string> _dict; // 哈希表
};
Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐