Linux TcpSocket编程

一.TCP套接字基础与echo系统

1.什么是TCP套接字

TCP 套接字(TCP Socket)是基于 TCP(传输控制协议)的网络通信接口,用于在网络中实现可靠的、面向连接的双向数据传输。它屏蔽了底层网络细节,让应用程序能通过简单接口进行跨网络通信。它的工作方式如下:

1.1创建套接字socket()

创建套接字对象,返回文件描述符

cpp 复制代码
int socket(int domain, int type, int protocol);
  • domain:协议族,TCP 用 AF_INET(IPv4)或 AF_INET6(IPv6)。
  • type:套接字类型,TCP 用 SOCK_STREAM(流式套接字,保证有序、可靠)。
  • protocol:指定协议,TCP 填 0(自动匹配 SOCK_STREAM 对应的 TCP)。

1.2绑定bind()

绑定本地 IP 地址和端口(服务器端必需,客户端可选)

cpp 复制代码
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
  • sockfdsocket() 返回的套接字文件描述符。
  • addr:结构体,包含本地 IP 和端口(如 struct sockaddr_in 用于 IPv4)。

1.3服务端监听listen()

服务器端监听端口,将套接字转为被动模式,等待客户端连接。

cpp 复制代码
int listen(int sockfd, int backlog);
  • backlog:最大等待连接队列长度(超过则新连接被拒绝)。
  • 当一个TCP服务器处于Listen状态,这个服务器就可以被连接(例如用telnet连接,或者用客户端连接).

1.4服务端接收客户端连接accept()

服务器端接收客户端连接,返回新的文件描述符(用于与该客户端通信)。

cpp 复制代码
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

注意:accept获取现有的连接,并且连接是从内核直接获取的,建立连接的过程与accept无关

对于accept的参数:当接收连接时,我们要知道连接来自哪(sockaddr)以及创建套接字的返回值sockfd。

最重要的是:accept的返回值,如果获取连接成功,该系统调用会返回一个合法整数------它也是一个文件描述符。

问题:为什么TCP在获取连接成功后,还会生成一个新的文件描述符?它和创建socket返回的文件描述符有什么区别?

这就好比:张三是在饭店外拉客的前台人员,当路上有路人经过时他就开始拉客,张三就属于被动连接的套接字;当拉客这一行为(accept)成功时,就创建新的服务员(新套接字)为连接服务。总的来说,监听套接字sockfd是连接入口 ,而accept返回的fd是通信通道

注意:当accept获取连接失败,不会阻塞或退出,他会立马等待下一个连接。

1.5客户端主动连接服务器connect()

cpp 复制代码
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
  • addr:服务器的 IP 和端口。

1.6收发数据**send()/recv()write()/read()**

cpp 复制代码
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
ssize_t recv(int sockfd, void *buf, size_t len, int flags);

1.7关闭套接字close()

cpp 复制代码
int close(int sockfd);

1.8使用过程

  1. 服务器端步骤

    • 调用 socket() 创建套接字。
    • 调用 bind() 绑定本地 IP 和端口。
    • 调用 listen() 开始监听。
    • 循环调用 accept() 接收客户端连接(返回新套接字)。
    • 通过新套接字用 recv()/send() 与客户端通信。
    • 通信结束后用 close() 关闭连接。
  2. 客户端步骤

    • 调用 socket() 创建套接字。
    • 调用 connect() 连接服务器的 IP 和端口。
    • 通过套接字用 send()/recv() 与服务器通信。
    • 通信结束后用 close() 关闭连接。

特点 :TCP 套接字通过三次握手建立连接,保证数据可靠传输(重传、排序、流量控制),适合需要可靠通信的场景(如 HTTP、文件传输)。

1.9三次握手

1. 服务器端准备(触发握手的前提)

服务器需先通过 socket()bind()listen() 完成初始化,处于 "监听状态",等待客户端连接:

cpp 复制代码
// 1. 创建监听套接字(未涉及握手,仅初始化资源)
int listen_fd = socket(AF_INET, SOCK_STREAM, 0);

// 2. 绑定本地 IP 和端口(确定握手的"目标端口")
struct sockaddr_in server_addr;
// 初始化 server_addr(设置 IP、端口等)
bind(listen_fd, (struct sockaddr*)&server_addr, sizeof(server_addr));

// 3. 进入监听状态(关键:此时套接字转为被动模式,允许接收连接请求)
listen(listen_fd, 5);  // 第二个参数为等待队列长度
  • 此时:服务器的 TCP 协议栈已准备好,可接收客户端的连接请求(三次握手的 "入口" 已打开)。
2. 客户端发起连接(第一次握手)

客户端通过 connect() 接口主动向服务器发起连接,触发第一次握手:

cpp 复制代码
// 1. 创建客户端套接字
int client_fd = socket(AF_INET, SOCK_STREAM, 0);

// 2. 初始化服务器地址(目标 IP 和端口)
struct sockaddr_in server_addr;
// 设置 server_addr 为服务器的 IP 和端口

// 3. 发起连接(触发第一次握手)
connect(client_fd, (struct sockaddr*)&server_addr, sizeof(server_addr));
  • connect() 调用后 :客户端 TCP 协议栈自动向服务器发送 SYN 报文 (第一次握手),包含客户端的初始序列号(seq = x)。
  • 此时客户端进入 SYN_SENT 状态,等待服务器响应。
3. 服务器接收请求并响应(第二次握手)

服务器通过 accept() 接口等待客户端连接,在底层收到 SYN 后自动完成第二次握手:

cpp 复制代码
// 服务器阻塞等待客户端连接(底层处理握手)
struct sockaddr_in client_addr;
socklen_t addr_len = sizeof(client_addr);
int client_fd = accept(listen_fd, (struct sockaddr*)&client_addr, &addr_len);
  • accept() 阻塞期间 :服务器 TCP 协议栈收到客户端的 SYN 报文后,会自动发送 SYN + ACK 报文 (第二次握手):
    • 包含服务器的初始序列号(seq = y);
    • 包含对客户端 SYN 的确认(ack = x + 1)。
  • 此时服务器进入 SYN_RCVD 状态,等待客户端的最终确认。
4. 客户端确认(第三次握手)

客户端收到服务器的 SYN + ACK 后,由 TCP 协议栈自动完成第三次握手,随后 connect() 返回:

  • 客户端 TCP 协议栈自动发送 ACK 报文 (第三次握手),包含对服务器 SYN 的确认(ack = y + 1)。
  • 此时客户端进入 ESTABLISHED 状态,connect() 调用成功返回(不再阻塞)。
5. 服务器完成握手(accept() 返回)

服务器收到客户端的 ACK 报文后:

  • 服务器 TCP 协议栈进入 ESTABLISHED 状态,完成三次握手。
  • 此时 accept() 调用成功返回,返回一个新的套接字(client_fd),用于与该客户端通信。

2.多版本的echo系统

2.1服务器端流程(TcpServer.cc

1. 初始化阶段
cpp 复制代码
// 创建监听socket
_listensockfd = socket(AF_INET, SOCK_STREAM, 0);

// 绑定地址
InetAddr local(_port);
bind(_listensockfd, local.NetAddrPtr(), local.NetAddrLen());

// 开始监听
listen(_listensockfd, backlog);
2. 运行阶段 - 三种并发模型

多进程版本

cpp 复制代码
            pid_t id = fork(); // 父进程
            if(id < 0)
            {
                LOG(LogLevel::FATAL) << "fork error";
                exit(FORK_ERR);
            }
            else if(id == 0)
            {
                // 子进程,子进程除了看到sockfd,能看到listensockfd吗??
                // 我们不想让子进程访问listensock!
                close(_listensockfd);
                if(fork() > 0) // 再次fork,子进程退出
                    exit(OK);

                Service(sockfd, addr); // 孙子进程,孤儿进程,1, 系统回收我
                exit(OK);
            }
            else
            {
                //父进程
                close(sockfd);

                //父进程是不是要等待子进程啊,要不然僵尸了
                pid_t rid = waitpid(id, nullptr, 0); // 阻塞的吗?不会,因为子进程立马退出了
                (void)rid;
            }

进程如果退出,文件默认会被自动释放,fd会自动关闭

该进程的子进程会继承父进程的资源,是可以拿fd进行访问的

换句话说,子进程可以看到父进程的sockfd和listensockfd。

当父子进程并发,子进程执行Service,父进程获取链接,需要各自关闭各自不需要的sockfd。

一个问题:子进程退出,父进程要等待,否则会变成僵尸进程,这样就不是串行了。

我们知道,子进程退出时会向父进程发送一个信号,只需要将这个信号的处理方式改为忽略即可,这样就实现了并发。

第二种解决方式:让子进程fork孙进程,然后自己马上退出。这样父进程就不会被阻塞,而孙进程变成了孤儿进程,会被系统回收。

多线程版本

两个问题:如果进程打开了一个文件,得到了一个fd,线程可以看到吗?

线程能关闭自己不需要的fd吗?

可以看到fd。但是他不能关闭!因为线程只是新增了pcb,还是共享一个地址空间,关闭fd会影响其他线程。

cpp 复制代码
ThreadData *td = new ThreadData(sockfd, addr, this);
            pthread_t tid;
            pthread_create(&tid, nullptr, Routine, td);

//子进程执行Routine方法
//Routine回调Service
 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;
    }

线程池版本

将Service作为任务入队列

cpp 复制代码
ThreadPool<task_t>::GetInstance()->Enqueue([this, sockfd, &addr](){
    this->Service(sockfd, addr);
});
3.服务处理
cpp 复制代码
void Service(int sockfd, InetAddr &peer) {
    while (true) {
        ssize_t n = read(sockfd, buffer, sizeof(buffer)-1);  // 读取请求
        std::string echo_string = _func(buffer, peer);       // 业务处理
        write(sockfd, echo_string.c_str(), echo_string.size()); // 返回响应
    }
}

2.2客户端流程(TcpClient.cc)

cpp 复制代码
// 创建socket
int sockfd = socket(AF_INET, SOCK_STREAM, 0);

// 连接服务器
InetAddr serveraddr(serverip, serverport);
connect(sockfd, serveraddr.NetAddrPtr(), serveraddr.NetAddrLen());

// 通信循环
while (true) {
    write(sockfd, line.c_str(), line.size());    // 发送请求
    read(sockfd, buffer, sizeof(buffer)-1);      // 接收响应
}

2.3设计细节

根据服务器无法拷贝的特性,让TcpServer继承NoCopy,为了达到无法拷贝,无法赋值的目的。

cpp 复制代码
class NoCopy
{
public:
    NoCopy(){}
    ~NoCopy(){}
    NoCopy(const NoCopy &) = delete;
    const NoCopy &operator = (const NoCopy&) = delete;
};

二.翻译系统与远程执行命令系统

切换不同的模块,只需要改变服务端的执行方法即可。

1.翻译系统

我们只要将上一章写的Dict.hpp模块导入,然后在服务端做如下改变即可:创建字典对象d,加载字典LoadDict,创建TcpServer对象,并用lambda传参,将消息的处理方法改为字典中的Translate方法。

cpp 复制代码
Dict d;
d.LoadDict();

 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();

2.远程执行命令系统

编写远程执行命令的模块:Command.hpp。

按照白名单的方式限制用户的命令。最核心的功能实现在于popen接口:

cpp 复制代码
FILE *popen(const char *command, const char *mode);
  • command:字符串,指定要执行的外部命令(如 "ls -l""grep hello")。
  • mode:字符串,指定管道的方向,只能是 "r""w"
    • "r":父进程从管道读取子进程的输出(子进程的 stdout 被重定向到管道)。
    • "w":父进程通过管道向子进程写入数据(子进程的 stdin 被重定向到管道)。

简单来说,popen的原理就是一个精简版的myshell:

  1. 创建管道(pipe) :调用 pipe() 系统调用创建一个匿名管道,包含两个文件描述符:fd[0](读端)和 fd[1](写端)。管道是内核中的一块缓冲区,用于进程间单向通信。

  2. 创建子进程(fork) :调用 fork() 创建子进程,子进程复制父进程的资源(包括管道的文件描述符)。

  3. 重定向子进程的 IO :根据 mode 参数修改子进程的文件描述符,将其标准输入 / 输出与管道绑定:

    • mode"r":子进程关闭管道的读端(fd[0]),将自己的标准输出(stdout,文件描述符 1)通过 dup2() 重定向到管道的写端(fd[1])。此后,子进程的输出会写入管道。
    • mode"w":子进程关闭管道的写端(fd[1]),将自己的标准输入(stdin,文件描述符 0)通过 dup2() 重定向到管道的读端(fd[0])。此后,子进程会从管道读取输入。
  4. 子进程执行命令(exec) :子进程调用 execl("/bin/sh", "sh", "-c", command, (char*)NULL) 执行外部命令:通过 shell 解析 command 字符串(支持管道、重定向等 shell 语法)。

  5. 父进程处理管道 :父进程关闭管道中未使用的一端(与子进程相反),并将另一端封装为 FILE* 流返回。例如:

    • mode"r":父进程关闭写端(fd[1]),用读端(fd[0])创建读流。
    • mode"w":父进程关闭读端(fd[0]),用写端(fd[1])创建写流。
  6. 通信与关闭 :父进程通过返回的 FILE* 流与子进程通信(读 / 写数据)。完成后需调用 pclose() 关闭流,pclose() 会等待子进程结束并释放资源。

执行命令模块:

cpp 复制代码
#pragma once

#include <iostream>
#include <string>
#include <cstdio>
#include <set>
#include "Command.hpp"
#include "InetAddr.hpp"
#include "Log.hpp"

using namespace LogModule;

class Command
{
public:
    // ls -a && rm -rf
    // ls -a; rm -rf
    Command()
    {
        // 严格匹配
        _WhiteListCommands.insert("ls");
        _WhiteListCommands.insert("pwd");
        _WhiteListCommands.insert("ls -l");
        _WhiteListCommands.insert("touch haha.txt");
        _WhiteListCommands.insert("who");
        _WhiteListCommands.insert("whoami");
    }
    bool IsSafeCommand(const std::string &cmd)
    {
        auto iter = _WhiteListCommands.find(cmd);
        return iter != _WhiteListCommands.end();
    }
    std::string Execute(const std::string &cmd, InetAddr &addr)
    {
        // 1. 属于白名单命令
        if(!IsSafeCommand(cmd))
        {
            return std::string("坏人");
        }

        std::string who = addr.StringAddr();

        // 2. 执行命令
        FILE *fp = popen(cmd.c_str(), "r");
        if(nullptr == fp)
        {
            return std::string("你要执行的命令不存在: ") + cmd;
        }
        std::string res;
        char line[1024];
        while(fgets(line, sizeof(line), fp))
        {
            res += line;
        }
        pclose(fp);
        std::string result = who + "execute done, result is: \n" + res;
        LOG(LogLevel::DEBUG) << result;
        return result;
    }
    ~Command()
    {}
private:
    // 受限制的远程执行
    std::set<std::string> _WhiteListCommands;
};

TcpServer需要做的改变:

cpp 复制代码
Command cmd;
  
std::unique_ptr<TcpServer> tsvr = std::make_unique<TcpServer>(port,
                                                                  std::bind(&Command::Execute, &cmd, std::placeholders::_1, std::placeholders::_2));
相关推荐
Eternal-Student8 小时前
【ubuntu】在Linux系统上安装Microsoft Edge浏览器
linux·ubuntu·microsoft
研來如此8 小时前
公网ip与内网ip
网络·tcp/ip
谅望者8 小时前
Linux文件查看命令完全指南:cat、less、head、tail、grep使用详解
linux·excel·less·shell·文件操作·命令行·系统运维
casdfxx8 小时前
blender实现手柄控制VR视角
linux·vr·blender
利刃大大8 小时前
【高并发服务器】十三、TcpServer服务器管理模块
服务器·高并发·项目·cpp
盼哥PyAI实验室9 小时前
纯前端打造个人成长网站:零后端、零部署、零服务器的实践分享
运维·服务器·前端·javascript·echarts·个人开发
信看9 小时前
树莓派 ADS1263 各种库程序
linux·运维·服务器
爱奥尼欧9 小时前
【Linux笔记】网络部分——传输层协议TCP(2)
linux·网络·笔记·tcp/ip
嵌入式小李.man9 小时前
linux中多路复用IO:select、poll和epoll
linux·c++