Linux操作系统学习记录之---TcpSocket

I- TCP Socket 网络接口

下面的函数全部声明在 <sys/socket.h>头文件中

I.1- socket

c++ 复制代码
int socket(int domain, int type, int protocol);

函数描述:

  • 打开一个网络通信端口 , 如果成功 , 就像open一样返回一个文件描述符 ; 如果失败 ,返回-1
  • 进程可以像读写普通文件一样使用readwrite 来从网络中读取和写入 (不准确)
  • 对于 IPV4 , 参数一domain 传入宏 AF_INET
  • 对于TCP协议 , 参数二 type 指定为宏 SOCK_STREAM , 表示面向字节流的传输协议
  • 参数三 protocal 直接填0 , 因为有了参数一和参数二 , 已经可以确定具体的协议类型.

I.2- bind

c++ 复制代码
int bind(int sockfd, const struct sockaddr *addr,socklen_t addrlen);
  • 用于将参数 sockfd 和 参数 addr 绑定在一起 , 即本地文件描述符网络地址信息 ; 这样可以让 sockfd 监听 addr所描述的ip地址和端口号.
  • 绑定成功时返回 0 , 失败时返回1.
  • 对于服务端 : 监视的网络地址和端口号往往不变 , 需要正常绑定.
  • 对于客户端 : 监视的网络地址和端口号随不同的服务端变化 , 因此不用显式绑定 , 而是让操作系统来默默绑定端口.

!bind的第二个参数

  • bind的第二个参数是一个通用类型的指针 , 指向一个网络结构体 , 描述了网络协议相关的信息 .
  • 这里的 struct sockaddr采用了 多态 的设计思想 , 作为基类 , 他可以接受多种协议的结构体 .
  • 对于此处的tcp协议来说 , 就是结构体 struct sockaddr_in , 在传入bind函数时需要强制类型转换 , 此结构体的初始化方式如下 :
c++ 复制代码
//对于服务端:
struct sockaddr_in local ;

    local = {}; //使用c++11的列表初始化(c语言就用memset)

    local.sin_family = AF_INET; //地址族

    local.sin_port = htons(port); //将端口号从主机序列转为网络序列

    local.sin_addr.s_addr = INADDR_ANY; //宏,表示本地的任意ip地址(因为本地可能                                          //有多张网卡和多个ip地址)
//对于客户端 : 
struct sockaddr_in server;
    server = {}; //同上
    server.sin_family = AF_INET; //同上
    server.sin_port = htons(port); //同上
    inet_pton(AF_INET,ip.c_str(),&server.sin_addr.s_addr);//需要指定服务端的                                                       //ip,并将其转化为网络序列

I.3- listen

c++ 复制代码
int listen(int sockfd, int backlog);
  • 用于声明 sockfd 处于监听状态 , 且最大允许 backlog 个客户端处于连接等待状态 , 如果接收到更多的请求就忽略 . (一般设置为5 , 过大的backlog对服务端性能要求高)
  • 监听成功返回0 , 失败则返回 -1;

I.4- accept(服务端)

c++ 复制代码
int accept(int sockfd, struct sockaddr *_Nullable restrict addr,
                  socklen_t *_Nullable restrict addrlen);
  • 三次握手 完成后 , 服务端调用 accept 函数接受客户端连接 ;
  • 如果暂时没有客户端请求 , 则阻塞等待
  • accept的第二和第三个参数是 输出型参数 , 如果成功接受客户端请求 , 会将服务端传入的 addraddrlen 填充为客户端的网络信息 .
  • accept 的返回值比较特殊 , 返回一个新的文件描述符 . 借助它 , 服务端就可以使用 readwrite 向客户端发送数据

I.5- connect(客户端)

c++ 复制代码
       int connect(int sockfd, const struct sockaddr *addr,
                   socklen_t addrlen);
  • 用于让客户端连接服务端 , 成功后返回0/失败则返回-1
  • 参数二和参数三需要接受 服务端网络信息 , 而非客户端自己

netstat -tnlp
telnet + "website" = 测试tcp连接是否建立
netstat -tnap | grep PORT

II- 多进程的TCP服务端和客户端

II.1- 一些设计细节

错误码(避免硬编码)

当然啦 , 真正写的时候还是有一个需求加一个需求 , 而不是真的像预知未来一样一次性写到位.

c++ 复制代码
enum ExitNum
{
    SocketErr,
    BindErr,
    ListenErr,
    ConnectErr,
    FileErr,
    ForkErr
};

II.1.1- 使用宏简化地址强转(图个方便)

网络地址的强转又臭又长 , 不妨用耿直的宏来拯救一下...

c++ 复制代码
#define CONV(addr) (struct sockaddr*)&addr

II.1.2- 特殊基类(禁用拷贝/赋值)

c++ 复制代码
//作为TcpServer的积累 , 优雅地禁用构造
class NoCopy
{
public:
    NoCopy() = default;
    NoCopy(const NoCopy& obj) = delete;
    NoCopy& operator=(const NoCopy& obj) = delete;
};

可以这样使用 :

c++ 复制代码
class TcpServer : public NoCopy
{
	
}

然后在TcpServer类里就没必要在担心拷贝和赋值的问题了.

II.2- 服务端(单进程版本)

II.3- tcp的独特函数接口:

II.4- 代码(简单echo):

c++ 复制代码
#pragma once

#include "Common.hpp"

class TcpServer : public NoCopy
{
private:
    void Service(int fd ,const AddrIn &addr)
    {
        char buffer[1024];
        while (true)
        {
            // 1,接受
            int num = read(fd, buffer, sizeof(buffer));

            if (num > 0)
            {
                buffer[num] = 0;
                std::cout << "服务端收到消息," << "来自:" << addr.GetInfo() << " 内容:" << buffer << std::endl;
            }
            else if (num == 0)
            {
                LOG(LogLevel::INFO) << "客户端全部退出\n";
                close(fd);
                return;
            }
            else
            {
                LOG(LogLevel::ERROR) << "server attempt to read , but failed\n";
                close(fd);
                return;
            }
            // 2,处理

            // 3,回复
            std::string message = "服务端收到了你的消息:" + std::string(buffer);
            num = write(fd, message.c_str(), message.size());
        }
    }

public:
    TcpServer(uint16_t port)
        : _port(port), _listen_fd(-1),_isRunning(false)
    {
    }
    void Init()
    {
        int check_ret = 666;
        // 1创建套接字
        _listen_fd = socket(AF_INET, SOCK_STREAM, 0);
        if (_listen_fd < 0)
        {
            LOG(LogLevel::ERROR) << "socket creattion failed ";
            exit(ExitNum::SocketErr);
        }
        LOG(LogLevel::INFO) << "套接字创建成功,fd = " << std::to_string(_listen_fd) << "\n";
        // 2,绑定
        AddrIn addr(_port);
        check_ret = bind(_listen_fd, addr.GetAddr(), addr.GetSize());
        if (check_ret < 0)
        {
            LOG(LogLevel::ERROR) << "bind failed";
            exit(ExitNum::BindErr);
        }
        LOG(LogLevel::INFO) << "和套接字:" << std::to_string(_listen_fd) << "绑定成功!!!\n";

        //2.5 (另外的补充)给socket套上复活甲
        int opt = 1;
        setsockopt(_listen_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
        // 3,监听
        check_ret = listen(_listen_fd, 8); // 注意:listen的参数二,代表了最大允许监听的端口
        if (check_ret < 0)
        {
            LOG(LogLevel::ERROR) << "listen failed";
            exit(ExitNum::ListenErr);
        }
        LOG(LogLevel::INFO) << "监听成功!!!\n";

        
    }
    void Start()
    {
        _isRunning = true;
        while (_isRunning)
        {
            // 1,建立连接
            struct sockaddr_in peer;
            socklen_t len = sizeof(peer);
            int fd = accept(_listen_fd, CONV(peer), &len);  //注意 : accept函数返回的fd才是通信中使用的.
            if (fd < 0)
            {
                LOG(LogLevel::WARNING) << "建立连接失败,开始重试\n";
                continue;
            }
            LOG(LogLevel::INFO) << "建立连接成功!!!\n";
            // 2,处理
            AddrIn addr(peer);
            Service(fd,addr);

        }
        _isRunning = false;
    }
    ~TcpServer()
    {
        close(_listen_fd);
    }

private:
    std::string _ip;
    uint16_t _port;
    int _listen_fd;

    bool _isRunning;
};

II.5- 软件telnet临时测试:

II.6- 客户端

II.7- 代码:

c++ 复制代码
#pragma once
#include "Common.hpp"

#include "Log.hpp"

using namespace LogModule;

class TcpClient
{
public:
    TcpClient(const std::string &ip, uint16_t port)
        : _ip(ip), _addr(ip, port) ,_port(port), _fd(socket(AF_INET, SOCK_STREAM, 0))
    {
        
    }
    void Init()
    {
        int ret = connect(_fd, _addr.GetAddr(), _addr.GetSize());
        if (ret < 0)
        {
            LOG(LogLevel::ERROR) << "connect failed!!!\n";
            exit(ExitNum::ConnectErr);
        }
    }

    void Run()
    {
        std::string input;
        char recv_buffer[256];
        while (std::getline(std::cin, input))
        {
            write(_fd, input.c_str(), input.size());

            int num = read(_fd , recv_buffer , sizeof(recv_buffer)-1); //注意,记得少接受一个字节,留出 \0的位置
            if(num > 0)
            {
                recv_buffer[num] = 0;
                std::cout << recv_buffer << std::endl;
            }
        }
    }
    ~TcpClient()
    {
    }

private:
    std::string _ip;
    AddrIn _addr;
    uint16_t _port;
    int _fd;
};

II.8- 测试

c++ 复制代码
#include"TcpClient.hpp"




int main(int argc , char* argv[])
{
    //1,处理命令行参数
    if(argc != 3)
    {
        LOG(LogLevel::ERROR) << "客户端参数传递错误\n";
        return 1;
    }
    std::string ip = argv[1];
    uint16_t port = std::stoi(argv[2]);


    TcpClient self(ip,port);

    self.Init();

    self.Run();
    
    return 0;
}

III- 服务端代码(非单线程,部分代码)

III.1- 方案一: 多进程

III.1.1- 注意点:

III.1.2- 代码:

c++ 复制代码
//TcpServer类
void Start()
    {
        _isRunning = true;
        while (_isRunning)
        {
            // 1,建立连接
            //略...
            // 2,处理
            AddrIn addr(peer);

            //a,多进程
            int pid = fork();
            if(pid == 0)
            {
                int n = fork();
                if(n == 0)
                {
                    close(_listen_fd);  //关闭子进程不必要的文件描述符
                    Service(fd,addr);
                }
                exit(0);
            }
            else if(pid > 0)
            {
                close(fd);
                int n = waitpid(pid,nullptr,0);
                (void)n;
            }
            else
            {
                LOG(LogLevel::ERROR) << "子进程创建失败\n";
                continue; 
            }
        }
        _isRunning = false;
    }

III.2- 方案二 : 多线程

III.2.1- 注意点:

III.2.2- 代码:

c++ 复制代码
//TcpServer类
void Start()
    {
        _isRunning = true;
        while (_isRunning)
        {
            // 1,建立连接
            //略...
            // 2,处理
            AddrIn addr(peer);

            //a,多进程
            pthread_t tid = -1;
            ThreadData* td = new ThreadData(this,addr,fd);  //注意 : 这是while循环,addr必须传值,而非地址.
            pthread_create(&tid , nullptr , Handler , td);

            // pthread_join(tid,nullptr);  //注意,如果使用join,就会阻塞!!! 所以还是得让线程自己detach
        }
        _isRunning = false;
    }

III.3- 方案三 : 线程池

III.3.1- 注意点:

III.3.2- 代码:

c++ 复制代码
//TcpServer类
void Start()
    {
        _isRunning = true;
        while (_isRunning)
        {
            // 1,建立连接
            //略...
            // 2,处理
            AddrIn addr(peer);

            //a,多线程
             ThreadPool<task_t>::GetInstance().EnQueue([this,fd,addr](){ 
                this->Service(fd,addr);
            }); //注意 : fd和addr必须值捕获,而非引用捕获
        }
        _isRunning = false;
    }

IV- 业务逻辑添加:远程指令执行:

Command的设计:

  • 构造函数 : 暴力一点 , 直接硬编码白名单指令
  • Execute : 接受 一串字符 , 返回执行结果 .

命令行解析方式:

  • 可以使用自定义shell的命令行解析代码(问题 : 缺乏权限限制)
  • 严格限制可供远程调用的命令!!!
  • 此处采用的解析方式 : 函数popen , 把解析工作交给系统

popen的基础用法:

c++ 复制代码
#pragma once 
#include<iostream>
#include<cstdio>
#include<set>
#include<string>

#include"Log.hpp"
using namespace LogModule;

#include"AddrIn.hpp"

class CmdExecutor
{
public:
    CmdExecutor()
    {
        _WhiteList.insert("ls");
        _WhiteList.insert("pwd");
        _WhiteList.insert("ll");
        _WhiteList.insert("tree");
        _WhiteList.insert("sl");
        _WhiteList.insert("whoami");
    }

    std::string Execute(const std::string& cmd , const AddrIn& addr)
    {
        std::string ret;
        //1,命令合法性校验
        auto it = _WhiteList.find(cmd); 

        if(it == _WhiteList.end())
        {
            std::cout << cmd << "群众里有坏人,居然想执行" << "[" << cmd <<"]" ;
            return std::string("群众里有坏人,居然想执行") + "[" + cmd + "]";
        }
        LOG(LogLevel::INFO) << "用户:" << addr.GetInfo() << "尝试执行命令:" << cmd << "\n";
        //2,命令解析(利用popen)
         char buffer[1204];
        FILE* pf = popen(cmd.c_str(),"r");
        if(pf == nullptr)  //注意 , 记得防御性编程
        {
            return "server busy";
        }
		//3,构造返回值
        while(fgets(buffer,sizeof(buffer),pf))
        {
            //std::cout << buffer;
            ret += buffer;
            //ret += "\n";  //fgets自带换行 , 不用自己加
        }
        LOG(LogLevel::INFO) << "一条命令执行完毕\n";
        //4,善后工作
        pclose(pf);
        return ret;
    }
    ~CmdExecutor()
    {}

private:
    std::set<std::string> _WhiteList;
};

V- 代码链接(我自己看的哈哈哈哈)

c++_linux · huangan/linux里的代码 - 码云 - 开源中国

huangan/linux里的代码 - Gitee.com

相关推荐
AOwhisky4 小时前
Linux逻辑卷管理:从“固定隔间”到“弹性存储池”的智慧
linux·运维·服务器
李白你好5 小时前
Burp Suite插件 | 高级HTTP头操作工具
网络·网络协议·http
AI视觉网奇5 小时前
ue5 插件 WebSocket
c++·ue5
左直拳5 小时前
将c++程序部署到docker
开发语言·c++·docker
凉、介5 小时前
深入 QEMU Guest Agent:虚拟机内外通信的隐形纽带
c语言·笔记·学习·嵌入式·虚拟化
崇山峻岭之间5 小时前
Matlab学习记录31
开发语言·学习·matlab
恒者走天下5 小时前
AI智能体通讯项目(底层AI通讯协议实现)
c++
英雄各有见5 小时前
Chapter 5.1.1: 编写你的第一个GPU kernel——Cuda Basics
c++·gpu·cuda·hpc
石像鬼₧魂石5 小时前
22端口(OpenSSH 4.7p1)渗透测试完整复习流程(含实战排错)
大数据·网络·学习·安全·ubuntu