当前位置: 首页 > news >正文

Linux TcpSocket编程

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

1.什么是TCP套接字

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

1.1创建套接字socket()

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

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 地址和端口(服务器端必需,客户端可选)

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

1.3服务端监听listen()

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

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

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

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

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

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

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

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

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() 完成初始化,处于 “监听状态”,等待客户端连接:

// 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() 接口主动向服务器发起连接,触发第一次握手:

// 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 后自动完成第二次握手:

// 服务器阻塞等待客户端连接(底层处理握手)
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. 初始化阶段
// 创建监听socket
_listensockfd = socket(AF_INET, SOCK_STREAM, 0);// 绑定地址
InetAddr local(_port);
bind(_listensockfd, local.NetAddrPtr(), local.NetAddrLen());// 开始监听
listen(_listensockfd, backlog);
2. 运行阶段 - 三种并发模型

多进程版本

            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会影响其他线程。

ThreadData *td = new ThreadData(sockfd, addr, this);pthread_t tid;pthread_create(&tid, nullptr, Routine, td);//子进程执行Routine方法
//Routine回调Servicestatic 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作为任务入队列

ThreadPool<task_t>::GetInstance()->Enqueue([this, sockfd, &addr](){this->Service(sockfd, addr);
});
3.服务处理
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)

// 创建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,为了达到无法拷贝,无法赋值的目的。

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

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

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

1.翻译系统

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

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接口:

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() 会等待子进程结束并释放资源。

执行命令模块:

#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 -rfCommand(){// 严格匹配_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需要做的改变:

Command cmd;std::unique_ptr<TcpServer> tsvr = std::make_unique<TcpServer>(port,std::bind(&Command::Execute, &cmd, std::placeholders::_1, std::placeholders::_2));

http://www.dtcms.com/a/560586.html

相关文章:

  • 怎么可以在百度发布信息seo won jin
  • TensorFlow深度学习实战——自定义图神经网络层
  • 车陂手机网站开发学校网站群建设必要
  • 【Elasticsearch入门到落地】18、Elasticsearch实战:Java API详解高亮、排序与分页
  • Java Web学习 第1篇前端基石HTML 入门与核心概念解析
  • Kafka4.1.0 队列模式尝鲜
  • transformer记录一(输入步骤讲解)
  • 做生存分析的网站有哪些网站背景怎么弄
  • Tomcat 新手避坑指南:环境配置 + 启动问题 + 乱码解决全流程
  • 整理、分类、总结与介绍Vue前端开发日常常用的第三方库/框架/插件-收藏
  • 第九天~在Arxml中定义一对XCP-PDU用于测量标定
  • Tomcat 配置问题速查表
  • 第九天~AUTOSAR网络管理NM-PDU详解:在Arxml中定义唤醒节点的NM-PDU
  • 在centos 7上配置FIP服务器的详细教程!!!
  • 做网站三网多少钱wordpress 贴吧主题
  • 无锡网站建设营销型诸城公司做网站
  • 【Docker】容器网络探索(二):实战理解 host 网络
  • 《数据结构风云》:二叉树遍历的底层思维>递归与迭代的双重视角
  • Java EE初阶 --多线程2
  • 论文精读(七):结合大语言模型和领域知识库的证券规则规约方法
  • Linux shell sed 命令基础
  • 选 Redis Stream 还是传统 MQ?队列选型全攻略(适用场景、优缺点与实践建议)
  • 【JVM】详解 Java内存模型(JMM)
  • 做网站工作室广告网站建设
  • 小语种网站制作广州网站建设哪里有
  • 广州学做网站上饶网站建设多少钱
  • GO写的http服务,清空cookie
  • 响应式企业网站模板望京网站建设公司
  • 最新聊天记录做图网站ip软件点击百度竞价推广
  • 关于学校网站建设申请报告深圳市网络seo推广价格