【Linux】Socket编程-TCP构建自己的C++服务器

🌈 个人主页:Zfox_

🔥 系列专栏:Linux

目录

  • [一:🔥 Socket 编程 TCP](#一:🔥 Socket 编程 TCP)
    • [🦋 TCP socket API 详解](#🦋 TCP socket API 详解)
    • [🦋 多线程远程命令执行](#🦋 多线程远程命令执行)
    • [🦋 网络版计算器(应用层自定义协议与序列化)](#🦋 网络版计算器(应用层自定义协议与序列化))
  • [二:🔥 共勉](#二:🔥 共勉)

一:🔥 Socket 编程 TCP

🦋 TCP socket API 详解

下面介绍程序中用到的 socket API,这些函数都在 sys/socket.h 中
socket

cpp 复制代码
#include <sys/types.h>
#include <sys/socket.h>

// 创建 socket 文件描述符 (TCP/UDP, 客户端 + 服务器)
int socket(int domain, int type, int protocol);

domain: 域 / 协议家族
	AF_INET      IPv4 Internet protocols
	AF_INET6     IPv6 Internet protocols

type: 报文类型
    SOCK_DGRAM      Supports datagrams (connectionless, unreliable messages of a fixed maximum length).
    SOCK_STREAM     Provides sequenced, reliable, two-way, connection-based byte streams.  An out-of-band data transmission mechanism may be supported.

protocol: 传输层类型
	默认为0

On success, a file descriptor for the new socket is returned.  On error, -1 is returned, and errno is set appropriately.

bind

cpp 复制代码
#include <sys/types.h>
#include <sys/socket.h>

// 绑定端口号 (TCP/UDP, 服务器)
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

On success, zero is returned.  On error, -1 is returned, and errno is set appropriately.


// 2. 填充网络信息,并bind绑定
// 2.1 没有把socket信息设置进入内核
struct sockaddr_in local;
bzero(&local, sizeof(local));       // string.h
local.sin_family = AF_INET;
local.sin_port = ::htons(_port);   // 要被发送给对方,既要发送到网络中!   主机序列转换为网络序列 大小端转换 网络中都是大端 #include <arpa/inet.h>
local.sin_addr.s_addr = ::inet_addr(_ip.c_str());    // 1. string ip -> 4bytes 2. network order   #include <sys/socket.h>    #include <netinet/in.h>  #include <arpa/inet.h>
local.sin_addr.s_addr = INADDR_ANY;

// 2.1 bind    这里设置进入内核
int n = ::bind(_sockfd, _addr.NetAddr(), _addr.NetAddrLen());

📚 我们之前在调用socket的时候,明明已经填充了一次 AF_INET, 为什么这里还需要一次呢?

创建套接字的时候填充的 AF_INET 是给操作系统文件系统里的网络文件接口,告诉我们的操作系统我们要创建一个网络的套接字。

这里则是用来填充 sockaddr_in 网络信息,只有套接字的结构和这里的结构一样,操作系统才能绑定成功。

📚 必带四件套

cpp 复制代码
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

listen

cpp 复制代码
#include <sys/types.h>          
#include <sys/socket.h>
       
int listen(int sockfd, int backlog);

sockfd:			 指定的套接字
backlog				 等待连接队列的最大长度。

On success, zero is returned.  On error, -1 is returned, and errno is set appropriately.

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

accept

cpp 复制代码
#include <sys/types.h>      
#include <sys/socket.h>

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

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

connect

cpp 复制代码
#include <sys/types.h>       
#include <sys/socket.h>

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

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

🦋 多线程远程命令执行

📚 代码结构

cpp 复制代码
C++
CommandExec.hpp  Common.hpp  Cond.hpp  InetAddr.hpp  Log.hpp  Makefile  
Mutex.hpp  TcpClient.cc  TcpServer.cc  TcpServer.hpp  Thread.hpp  ThreadPool.hpp

TcpServer.hpp

cpp 复制代码
#pragma once

#include <iostream>
#include <cstring>
#include <string>
#include <cerrno>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <sys/wait.h>
#include <signal.h>
#include <pthread.h>

#include "Log.hpp"
#include "Common.hpp"
#include "InetAddr.hpp"
#include "ThreadPool.hpp"

using namespace LogModule;
using namespace ThreadPoolModule;

static const uint16_t gport = 8080;
using handler_t = std::function<std::string(std::string)>;

#define BACKLOG 8

class TcpServer
{
    using task_t = std::function<void()>;
    struct ThreadData
    {
        int sockfd;
        TcpServer *self;
    };
public:
    TcpServer(handler_t handler, int port = gport)
        : _handler(handler), 
        _port(port), 
        _isrunning(false)
    {
    }

    bool InitServer()
    {
        // 1. 创建tcp socket
        _listensockfd = ::socket(AF_INET, SOCK_STREAM, 0); // Tcp Socket
        if (_listensockfd < 0)
        {
            LOG(LogLevel::FATAL) << "_listensockfd error";
            Die(SOCKET_ERR);
        }
        LOG(LogLevel::INFO) << "_listensockfd create success, _listensockfd is : " << _listensockfd;

        struct sockaddr_in local;
        memset(&local, 0, sizeof(local));
        local.sin_family = AF_INET;
        local.sin_port = htons(_port);
        local.sin_addr.s_addr = INADDR_ANY;

        // 2. bind
        int n = ::bind(_listensockfd, CONV(&local), sizeof(local));
        if (n < 0)
        {
            LOG(LogLevel::FATAL) << "bind error";
            Die(BIND_ERR);
        }
        LOG(LogLevel::INFO) << "bind success, _listensockfd is : " << _listensockfd;

        // 3. cs tcp是面向连接的,就要求tcp随时随地等待被连接
        // tcp 需要将socket设置成为监听状态
        n = ::listen(_listensockfd, BACKLOG);
        if (n < 0)
        {
            LOG(LogLevel::FATAL) << "listen error";
            Die(LISTEN_ERR);
        }
        LOG(LogLevel::INFO) << "listen success, _listensockfd is : " << _listensockfd;

        //::signal(SIGCHLD, SIG_IGN);     // 子进程退出,OS会自动回收资源,不用再wait了
        return true;
    }

    void HandlerRequest(int sockfd)   // TCP 也是全双工通信
    {
        LOG(LogLevel::INFO) << "HandlerRequest, sockfd is : " << sockfd;
        char inbuffer[4096];
        // 长任务
        while(true)
        {
            ssize_t n = ::recv(sockfd, inbuffer, sizeof(inbuffer) - 1, 0);
            if(n > 0)
            {
                LOG(LogLevel::INFO) << inbuffer;

                inbuffer[n] = 0;
                // std::string echo_str = "server echo# ";
                // echo_str += inbuffer; 

                std::string cmd_result = _handler(inbuffer);

                ::send(sockfd, cmd_result.c_str(), cmd_result.size(), 0);
            }
            else if(n == 0)
            {
                // read 如果读取返回值是0,表示client退出
                LOG(LogLevel::INFO) << "client quit: " << sockfd;
                break;
            }
            else {
                // 读取失败了
                break;
            }
        }

        ::close(sockfd);   // fd泄露问题
    }

    static void *ThreadEntry(void *args)
    {
        pthread_detach(pthread_self());
        ThreadData* data = (ThreadData*)args;
        data->self->HandlerRequest(data->sockfd);
        delete data;
        return nullptr;
    }

    void Start()
    {
        _isrunning = true;
        while (_isrunning)
        {
            // 不能直接读取数据
            // 1. 获取新连接
            struct sockaddr_in peer;
            socklen_t peerlen = sizeof(peer);
            LOG(LogLevel::DEBUG) << "accept ing ...";
            // 我们要获取客户端的信息:数据(sockfd) + client socket信息(accept)
            int sockfd = ::accept(_listensockfd, CONV(&peer), &peerlen);
            if (sockfd < 0)
            {
                LOG(LogLevel::WARNING) << "accept error" << strerror(errno);
                continue;
            }

            // 获取连接成功了
            LOG(LogLevel::INFO) << "accept success, socket is : " << sockfd;
            InetAddr addr(peer);
            LOG(LogLevel::INFO) << "client info: " << addr.Addr();

            // version-0
            // HandlerRequest(sockfd);


            // version-1 多进程版本
            // pid_t id = fork();
            // if(id == 0)
            // {
            //     // child
            //     // 问题1: 父进程的文件描述符表子进程会继承 父子各一张共两张 
            //     // 1.关闭不需要的fd
            //     ::close(_listensockfd);
            //     if(fork() > 0) exit(0); // 子进程退出
            //     // 孙子进程 -> 孤儿进程 -> 1
            //     HandlerRequest(sockfd);
            //     exit(0);
            // }
            // ::close(sockfd);    // 父进程也关闭不需要的 已经交给子进程了

            // // 不会阻塞
            // pid_t rid = ::waitpid(id, nullptr, 0);
            // if(rid < 0)
            // {
            //     LOG(LogLevel::WARNING) << "waitpid error";
            // }



            // version-2 多线程版本
            // pthread_t tid;
            // ThreadData* data = new ThreadData;
            // data->sockfd = sockfd;
            // data->self = this;
            // pthread_create(&tid, nullptr, ThreadEntry, data);    // 主线程和新线程是如何看待,文件描述符表, 共享一张文件描述符表!!属于同一个进程 !



            // version-3 线程池版本  一般用于短任务(注册登录),少量用户
            // task_t f = std::bind(&TcpServer::HandlerRequest, this, sockfd);     // 构建任务
            // ThreadPool<task_t>::getInstance()->Equeue(f);
            ThreadPool<task_t>::getInstance()->Equeue([this, sockfd](){
                this->HandlerRequest(sockfd);
            });
        }
    }

    void Stop()
    {
        _isrunning = false;
    }

    ~TcpServer()
    {
    }

private:
    int _listensockfd; // 监听socket
    uint16_t _port;
    bool _isrunning;

    // 处理上层任务的入口
    handler_t _handler;
};

TcpServer.cc

cpp 复制代码
#include "TcpServer.hpp"
#include "CommandExec.hpp"
#include <memory>

using namespace LogModule;

int main()
{
    ENABLE_CONSOLE_LOG();
    Command cmd;
    
    std::unique_ptr<TcpServer> tsvr = std::make_unique<TcpServer>([&cmd](std::string cmdstr){
        return cmd.Execute(cmdstr);
    });

    tsvr->InitServer();
    tsvr->Start();

    return 0;
}

TcpClient.cc

cpp 复制代码
#include <iostream>
#include <string>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <cstring>

// ./client_tcp serverip serverport
int main(int argc, char *argv[])
{
    if(argc != 3)
    {
        std::cout << "Usage: " << argv[0] << " serverip serverport" << std::endl;
        return 1;
    }
    std::string serverip = argv[1];
    int server_port = std::stoi(argv[2]);

    int sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if(sockfd < 0)
    {
        std::cout << "Create socket failed." << std::endl;
        return 2;
    }

    struct sockaddr_in server_addr;
    memset(&server_addr, 0, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(server_port);    
    server_addr.sin_addr.s_addr = inet_addr(serverip.c_str());

    // client 不需要显示的进行bind, tcp是面向连接的, connect 底层自动会进行bind
    int n = ::connect(sockfd, (struct sockaddr*)&server_addr, sizeof(server_addr));
    if(n < 0)
    {
        std::cout << "Connect to server failed." << std::endl;
        return 3;
    }

    // echo client
    std::string message;
    while(true)
    {
        char inbuffer[1024];
        std::cout << "input message: ";
        std::getline(std::cin, message);

        n = ::write(sockfd, message.c_str(), message.size());
        if(n > 0)
        {
            int m = ::read(sockfd, inbuffer, sizeof(inbuffer));
            if(m > 0)
            {
                inbuffer[m] = 0;
                std::cout << inbuffer << std::endl;
            }
            else break;
        }
        else break;
    }


    ::close(sockfd);
    return 0;   
}

CommandExec.hpp

cpp 复制代码
#pragma once

#include <iostream>
#include <string>
#include <cstdio>
#include <set>

const int line_size = 1024;

class Command
{
public:
    Command()
    {
        _white_list.insert("ls");
        _white_list.insert("pwd");
        _white_list.insert("ls -l");
        _white_list.insert("who");
        _white_list.insert("whoami");
        _white_list.insert("ll");
    }

    bool SafeCheck(const std::string& cmdstr)
    {
        auto iter = _white_list.find(cmdstr);
        return iter == _white_list.end() ? false : true;
    }

    // 给你一个命令字符串"ls -l",执行它并返回执行结果
    std::string Execute(std::string cmdstr)
    {
        // 1. pope
        // 2.fork + dup2(pipe[1], 1) + exec*, 执行结果给父进程, pipe[0]
        // 3. return 
        // FILE *popen(const cahr *command, const char *type);
        // pclose(FILE *stream);

        if(!SafeCheck(cmdstr))
        {
            return std::string(cmdstr + "不支持");
        }

        FILE *fp = popen(cmdstr.c_str(), "r");
        if(fp == nullptr)
        {
            return std::string("Failed");
        }

        char buffer[line_size];
        std::string result;
        while(true)
        {
            char *ret = ::fgets(buffer, sizeof(buffer), fp);
            if(!ret) break;
            result += ret;
        }

        pclose(fp);
        return result.empty() ? std::string("Done") : result;
    }
private:
    std::set<std::string> _white_list;
};

CommandExec.hpp

cpp 复制代码
#pragma once

#include <iostream>
#include <string>
#include <cstdio>
#include <set>

const int line_size = 1024;

class Command
{
public:
    Command()
    {
        _white_list.insert("ls");
        _white_list.insert("pwd");
        _white_list.insert("ls -l");
        _white_list.insert("who");
        _white_list.insert("whoami");
        _white_list.insert("ll");
    }

    bool SafeCheck(const std::string& cmdstr)
    {
        auto iter = _white_list.find(cmdstr);
        return iter == _white_list.end() ? false : true;
    }

    // 给你一个命令字符串"ls -l",执行它并返回执行结果
    std::string Execute(std::string cmdstr)
    {
        // 1. pope
        // 2.fork + dup2(pipe[1], 1) + exec*, 执行结果给父进程, pipe[0]
        // 3. return 
        // FILE *popen(const cahr *command, const char *type);
        // pclose(FILE *stream);

        if(!SafeCheck(cmdstr))
        {
            return std::string(cmdstr + "不支持");
        }

        FILE *fp = popen(cmdstr.c_str(), "r");
        if(fp == nullptr)
        {
            return std::string("Failed");
        }

        char buffer[line_size];
        std::string result;
        while(true)
        {
            char *ret = ::fgets(buffer, sizeof(buffer), fp);
            if(!ret) break;
            result += ret;
        }

        pclose(fp);
        return result.empty() ? std::string("Done") : result;
    }
private:
    std::set<std::string> _white_list;
};

🦋 网络版计算器(应用层自定义协议与序列化)

代码结构

cpp 复制代码
C++
Calculator.hpp  Common.hpp  Cond.hpp  Deamon.hpp  InetAddr.hpp  Log.hpp  Makefile  Mutex.hpp  
Protocol.hpp  TcpClient.cc  TcpServer.cc  TcpServer.hpp  Thread.hpp  ThreadPool.hpp
// 简单起见, 可以直接采用自定义线程
// 直接 client<<->>server 通信, 这样可以省去编写没有干货的代码

网络版计算器(应用层自定义协议与序列化)

二:🔥 共勉

以上就是我对 【Linux】Socket编程-TCP构建自己的C++服务器 的理解,想要完整代码可以私信博主噢!觉得这篇博客对你有帮助的,可以点赞收藏关注支持一波~😉

相关推荐
阿^觅13 分钟前
基础IO -- 动静态库(1)
linux·服务器
Channing Lewis15 分钟前
Python 3.9及以上版本支持的新的字符串函数 str.removeprefix()
服务器·python
隼玉18 分钟前
【STM32-学习笔记-11-】RTC实时时钟
c语言·笔记·stm32·学习
生活很暖很治愈32 分钟前
从玩具到工业控制--51单片机的跨界传奇【3】
c语言·嵌入式硬件·51单片机
抠脚学代码35 分钟前
LINUX 实现终端动态进度条记录
linux·运维·服务器·进度条
bohu8335 分钟前
ubuntu 22 安装vmware 17.5
linux·ubuntu·vmware·vmmon·vmnet
Xiao Xiangζั͡ޓއއ38 分钟前
于灵动的变量变幻间:函数与计算逻辑的浪漫交织(上)
c语言·开发语言·程序人生·学习方法·改行学it
可涵不会debug1 小时前
Git在码云上的使用指南:从安装到推送远程仓库
linux·运维·服务器·c++·git
hunandede1 小时前
Linux中的nc命令是网络工具中的一种,用于进行网络连接和数据传输。下面是nc命令的使用方法和实例:
linux·运维·服务器
Flocx1 小时前
联合体(Union)
开发语言·网络·c++·stm32