Linux网络相关概念和重要知识(3)(TCP套接字编程、远程命令的实现)

devtools/2025/3/30 5:17:15/

1.TCP套接字编程

(1)和UDP的区别

TCP和UDP都是传输层的协议,这两个协议有着自己的特点,会体现在代码上。但也有很多是不变的。想要通信,都需要创建socket文件(TCP也是通过socket通信的),并且都需要创建sockaddr_in用于网络通信的本地信息保存(服务器要显式bind),包括sin_port端口号,sin_family协议家族,sin_addr.s_addr存储的IP。

不同点:

TCP的性质是面向连接、可靠传输、面向字节流。

UDP的性质是不面向连接、不可靠传输、面向数据报。

前面我们已经体会过UDP编程的效果了,UDP的无连接表现在客户端随时启动,绑定成功后就能够向服务器随时发信息,服务器也能接收。而TCP的面向连接就需要客户端先和服务器建立连接,连接成功才能通信,因此我们也能知道TCP下的服务端会随时随地等待被连接。

UDP面向数据报体现在代码中就是传输数据时使用的是recvfrom、sendto函数(传输的对象是数据报),而TCP面向字节流就可以像管道那样使用系统调用read和write来读写,后面会讲到。

后续的讲解会从UDP和TCP的不同点出发,大部分思路相同,小部分处理不同

(2)面向连接的特性

①socket参数调整

相比较于UDP,只需修改第二个参数type为SOCK_STREAM即可,其余不变,这样创建的socket就是TCP的套接字了。

②服务器的监听状态(listen)

TCP服务器需要将socket设置为监听状态,随时等待别人来连接。

其中第二个参数我们暂时了解,填入一个数字即可,后面会专门讲解。

这一步之后服务器就被设置为监听状态了。

③服务器获取新连接(accept)

服务器启动要获取新连接,需要使用函数accept

现在需要对这个fd进行解释。函数参数里面的fd是正在监听的socket的fd,就好比在餐厅外面拉客的服务员。当拉客成功后,就会返回一个fd,这个fd相当于餐馆内部的服务员,和外面拉客的服务员不一样。这个新的返回的fd就是负责真正数据通信的文件描述符了,而那个监听的fd会继续监听,在餐馆外面拉客,和餐厅里面提供服务的fd不一致也不冲突。

④客户端尝试连接(connect)

(3)总结

2.远程命令的实现

远程命令,即让远程主机返回本地输入的结果。和聊天室的逻辑几乎一致。

有的命令很危险,可以通过黑名单(哪些不能执行)和白名单(哪些命令可以执行)来实现保护

(1)服务器端和客户端思路

服务器端:

先是让socket进入监听状态,然后进入死循环不断accept,当accept成功后就由多线程执行通信,主线程继续回到循环中accept。采用多线程的方法让一个服务器同时服务多个客户端是核心思想。除此之外,多进程、多进程多线程都可以,实现上略有差异。

每个线程负责recv和send,就像文件操作那样。其中执行命令采用的是popen和pclose,后续可在代码中看到。

客户端:connect服务器,连接成功后直接利用双线程,一个负责写一个负责读即可,这和聊天室的逻辑一致。

(2)相关代码

①TcpServer.hpp

#pragma once#include <iostream>
#include <string>
#include <sys/socket.h>
#include <sys/types.h>
#include <stdint.h>
#include <netinet/in.h>
#include <strings.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <cstdlib>
#include <deque>
#include <functional>
#include "myLog.hpp"
#include "myInetAddr.hpp"
#include "myCommon.hpp"
#include "myThreadPool.hpp"
#include "myCommand.hpp"using namespace std;
using namespace myLogModule; // 使用日志模块
using namespace myThreadPoolModule;
using namespace myCommandMoudle;const uint16_t default_port = 9000; // 默认端口号,无需指定IP
const int max_size = 1024;			// 存储信息的最大字节数class TcpServer
{
public:struct ClientInfo{myInetAddr client_inet_addr; // 客户端的IP和端口号int client_fd;				 // 客户端的socket文件描述符};TcpServer(uint16_t port = default_port): _listen_fd(-1),	  // 服务端的socket文件描述符_addr_server(port), // 服务端的端口号,默认为default_port,用于构造myInetAddr对象,自动初始化IP和端口号_isrunning(false)	  // 记录当前服务端的运行状态//_addr_client_list() // 客户端的IP和端口号列表,不需要设置,需要服务器端accept连接之后才知道客户端的IP和端口号{// socket创建网络通信的文件_listen_fd = socket(AF_INET, SOCK_STREAM, 0); // 网络通信,TCP通信方式,0默认if (_listen_fd == -1)						  // 创建失败返回-1{LOG(FATAL) << "服务端初始化失败"; // 输出错误信息exit(SERVER_ERROR);				  // 直接退出程序,1表示服务端的异常退出}if (bind(_listen_fd, _addr_server.Get_Const_Sockaddr_ptr(), _addr_server.Get_Socklen()) == -1) // 绑定服务端的IP和端口号{LOG(FATAL) << "服务端绑定失败";exit(SERVER_ERROR); // 直接退出程序,1表示服务端的异常退出}listen(_listen_fd, 8); // 监听连接LOG(INFO) << "服务端初始化成功,已启动监听端口" << (int)default_port;}void start(){_isrunning = true; // 启动服务器myThreadPool<task_t>::GetInstance()->StartAddTask(); // 启动线程池while (_isrunning) // 服务器运行时一直循环{sockaddr_in client_addr; // 客户端的IP和端口号,用于myInetAddr设置socklen_t client_addr_len = sizeof(client_addr);int client_fd = -1; // 用于服务的fd,之后向这个fd里面写内容就可以实现通信LOG(INFO) << "客户端现在可以连接服务器,正在等待...";// 等待客户端连接while ((client_fd = accept(_listen_fd, (sockaddr *)&client_addr, &client_addr_len)) == -1) // 等待客户端连接{LOG(WARNING) << "等待客户端连接失败,正在重试...";sleep(1); // 等待1秒}ClientInfo client_info{myInetAddr(client_addr), client_fd}; // 客户端的IP和端口号,客户端的fdLOG(INFO) << "客户端" << client_info.client_inet_addr.Get_Ip() << ":" << client_info.client_inet_addr.Get_Port() << "连接成功";// 利用bind调整函数参数myThreadPool<task_t>::GetInstance()->AddTask(bind(&TcpServer::ClientCom, this, client_info)); // 将客户端的fd添加到线程池中,执行ClientCom函数}}void ClientCom(ClientInfo client_info) // 服务器直接向这个sockfd里面写数据即可实现通信{while (_isrunning) // 服务器运行时一直循环{bzero(_read_buffer, sizeof(_read_buffer)); // 对读写数组清0,数组名就是数组的首地址_write_buffer.clear();					   // 对写数组清空// 会被阻塞在这个函数,直到收到数据,这个函数会将收到的数据写入_read_bufferssize_t n = recv(client_info.client_fd, _read_buffer, sizeof(_read_buffer) - 1, 0);if (n > 0){_read_buffer[n] = '\0'; // 读到的最后一个字符后面加上'\0',保证字符串安全if (strcmp(_read_buffer, "QUIT") == 0){LOG(INFO) << "客户端" << client_info.client_inet_addr.Get_Ip() << ":" << client_info.client_inet_addr.Get_Port() << "已断开连接";close(client_info.client_fd); // 直接关闭对应的fdreturn;						  // 该线程直接结束执行}_write_buffer = "客户端" + client_info.client_inet_addr.Get_Ip() + ":" + to_string(client_info.client_inet_addr.Get_Port()) + ":" + myCommand(_read_buffer).execute(); // 向客户端发送执行命令的结果send(client_info.client_fd, _write_buffer.c_str(), _write_buffer.size(), 0); // 向客户端发送执行命令的结果}}}void stop(){_isrunning = false; // 服务器停止,自动根据while退出循环}~TcpServer(){if (_listen_fd != -1){close(_listen_fd);LOG(INFO) << "已结束该端口的监听状态,服务端退出";_listen_fd = -1; // 防止二次调用多次关闭}}private:int _listen_fd;				 // 调用socket之后创建文件后返回的文件fd,用于监听连接bool _isrunning;			 // 记录当前服务端的运行状态myInetAddr _addr_server;	 // 服务端的IP和端口号string _write_buffer;		 // 服务器准备写出的信息char _read_buffer[max_size]; // 服务器读到的信息
};

②myCommand.hpp

#include <iostream>
#include <string>
using namespace std;namespace myCommandMoudle
{class myCommand // 用临时对象的方式实现命令执行{public:myCommand(const string &command): _command(command), _command_found(false){}string execute(){FILE *fp = popen(_command.c_str(), "r"); // 只读执行命令if (nullptr == fp){return "执行命令失败";}char tmp[1024];while (fgets(tmp, sizeof(tmp), fp) != nullptr){_result += tmp;_command_found = true;}if (!_command_found){_result = "command not found";}pclose(fp);return _result;}private:bool _command_found;string _command;string _result;};}

③TcpClientMain.cc

#include <iostream>
#include <cstring>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <signal.h>
#include <unistd.h>
#include "myCommon.hpp"using namespace std;int sockfd = socket(AF_INET, SOCK_STREAM, 0); // 流式套接字,用于TCP连接
// 网络服务,sockaddr_in写入要连接的服务器的IP和端口号
sockaddr_in server; // 可以向指定IP和端口号发送信息void ClientQuit(int signal)
{string message = "QUIT";send(sockfd, message.c_str(), message.size(), 0);cout << "你已退出聊天室" << endl;exit(0);
}void *ReceiveMessage(void *args)
{while (1){char read_buffer[1024];int n = recv(sockfd, read_buffer, sizeof(read_buffer) - 1, 0); // 已经和服务器建立连接了,不需要再去获取服务器的IP和端口号read_buffer[n] = '\0';cerr << read_buffer << endl; // 输出接收到的信息,用错误流接收,后面专门用重定向设置一个聊天界面}
}// CS模式,client和server,client发送消息,server接收消息,服务器端永远不会主动发送消息,都是被动的
int main(int argc, char *argv[]) // 第二个参数是IP,第三个参数是端口号
{if (argc != 3){cerr << "共需要传入三个参数,一个IP,一个端口号" << endl;exit(CLIENT_ERROR); // 表示客户端错误}if (sockfd < 0){cerr << "客户端启动失败" << endl;exit(CLIENT_ERROR);}string server_ip = argv[1];uint16_t server_port = stoi(argv[2]);cout << "客户端启动成功" << endl;memset(&server, 0, sizeof(server)); // 用0初始化,0是char类型的0,即0x00server.sin_family = AF_INET;server.sin_port = htons(server_port);                  // 保证字节序server.sin_addr.s_addr = inet_addr(server_ip.c_str()); // 保证字节序signal(2, ClientQuit); // 捕获ctrl+c信号,退出程序,触发信号会执行ClientQuit函数,这个函数会向服务器发送QUIT信息,服务器会删除用户信息connect(sockfd, (const sockaddr *)(&server), sizeof(server)); // 连接服务器,TCP连接,需要先建立连接,才能发送和接收信息cout << "连接服务器成功" << endl;pthread_t tid;pthread_create(&tid, nullptr, ReceiveMessage, nullptr);while (true){cout << "请输入命令:";string message;getline(cin, message); // 获取信息// 不需要绑定socket,直接向文件写信息即可,client也有自己的属性,但IP和端口号不需要显式调用bind,客户端指定端口号可能会冲突,首次sendto会自动绑定// 客户端自动bind,一个端口号只能bind一次,一个进程可以绑定多个端口号send(sockfd, message.c_str(), message.size(), 0); // connect连接好了之后,就可以向服务器发送信息了,TCP协议}return 0;
}

http://www.ppmy.cn/devtools/171222.html

相关文章

什么是AI训练师?未来将如何发展?

AI训练师&#xff08;AI Trainer&#xff09;是AI时代催生的新型技术角色&#xff0c;专注于从数据到模型的全生命周期培育&#xff0c;其核心使命是用数据喂养AI&#xff0c;用反馈优化模型。以下是结构化解析&#xff1a; 一、AI训练师的定位与价值 1. 角色定位 • 技术翻译…

2025 使用docker部署ubuntu24容器并且需要ubuntu24容器能通过ssh登录SSH 登录的Ubuntu24容器

以下是使用 Docker 部署可 SSH 登录的 ubuntu24 容器的步骤&#xff1a; 1.创建 Dockerfile&#xff08;保存为 Dockerfile.ubuntu24&#xff09; vim Dockerfile.ubuntu24 #复制如下内容 # 使用 Ubuntu 24.04 作为基础镜像 FROM ubuntu:24.04# 更新软件包列表并安装必要的软…

卷积神经网络 - 进一步理解反向传播

上一博文&#xff0c;我们学习了卷积神经网络的梯度和反向传播算法&#xff0c;本文我们来通过详细的推导和示例&#xff0c;结合卷积神经网络中卷积层与池化层&#xff08;汇聚层&#xff09;的反向传播过程&#xff0c;进一步加深对卷积神经网络的反向传播的理解。 建议大家…

鸿蒙特效教程10-卡片展开/收起效果

鸿蒙特效教程10-卡片展开/收起效果 在移动应用开发中&#xff0c;卡片是一种常见且实用的UI元素&#xff0c;能够将信息以紧凑且易于理解的方式呈现给用户。 本教程将详细讲解如何在HarmonyOS中实现卡片的展开/收起效果&#xff0c;通过这个实例&#xff0c;你将掌握ArkUI中状…

【QT】QTCreator测试程序

使用QTCreator实现窗体&#xff0c;其中拟合程度图左侧是测点列表&#xff0c;右侧是改测点的拟合程度图&#xff08;不使用UI&#xff0c;使用代码编写实现&#xff09; 实现思路 创建主窗口&#xff1a;继承 QMainWindow 类来创建主窗口。布局管理&#xff1a;使用 QSplitt…

JAVA学习*Object类

Object类 Object类是所有类的父类 类中有一些方法&#xff08;都需要掌握&#xff09; toString()方法 在学习类的对象的时候有介绍过了&#xff0c;当我们重新给此方法就会打印类与对象的信息 equals()方法 在Java中的比较&#xff0c; 如果左右两侧是基本类型变量&#…

【VolView】纯前端实现CT三维重建-CBCT

文章目录 什么是CBCTCBCT技术路线使用第三方工具使用Python实现使用前端实现 纯前端实现方案优缺点使用VolView实现CBCT VolView的使用1.克隆代码2.配置依赖3.运行4.效果 进阶&#xff1a;VolView配合Python解决卡顿1.修改VtkThreeView.vue2.新增Custom3DView.vue3.Python生成s…

QT-LINUX-Bluetooth蓝牙开发

BlueToothAPI QT-BlueToothApi Qt Bluetooth 6.8.2 官方提供的蓝牙API不支持linux。 D-Bus的API实现蓝牙 确保系统中安装了 BlueZ(版本需≥5.56),并且 Qt 已正确安装并配置了 D-Bus 支持。 默默看了下自己的版本.....D-BUS的API也不支持。 在 D-Bus 中,org 目录是 D-Bus…