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

Socket基础

Linux:socket

Socket理论概念:

补充知识:

我们说了那么多网络传输数据,但说再多,数据传输并不是目的,而是手段。我们的目的是通过数据传输从而拿到数据,对数据进行各种操作。

上图主机A是客户端,主机B是服务端。客户端向服务器发送数据,等待服务端数据处理,而服务端接收到客户端的数据后进行处理再发回给客户端,本质还是属于通信,而无论是客户端还是服务端,本质都属于进程。

因此,网络通信本质还是属于进程间通信。

人们上网只有两种行为,一是从远端服务获取数据,二是将本地数据上传到远端服务。所以数据传输不是目的,而是手段,数据到达主机内部后,交给主机内部的进程才是目的。

端口号认识:

端口号(port)是传输层协议的内容。是网络通信中用于区分同一台设备上不同网络应用程序的数字标识,本质上是一个 16 位的整数(范围 0-65535)。

端口号用于标识一个主机内的网络进程,通常一个进程只有一个端口号。但不是所有端口号都可以使用,有些端口号会与一些知名的协议进行固定绑定。简单理解来说就像生活中我们不能拿110,119作为我们电话号码,这些都是官方规定的具有特殊意义的电话号码。

那么既然端口号是用来标识进程的,而我们知道进程中也有一个PID同样也是用来标识进程,那么端口号与PID的区别是什么?

  1. 首先不是所有基础进程,都需要网络通信
  2. 虽从技术角度用PID来代替端口号是可行的,但假设如果有一天我们不适用PID来标识进程唯一值,那么网络部分怎么办。所以PID是系统部分概念,而端口号是网络的概念,两个分别代表不同的含义,就能进行更好的解耦合

简单理解端口号的底层实现:

在传输层中有一个hash表,是一个开散列式的hash,而假设端口号为8080,就会映射到其中一个hash元素中,当主机拿到报文之后,在传输层解包时,就会拿到该报文的端口号,接着再根据这个端口号映射到hash数组里查找与该端口号对应的进程,进而拿到数据。

结论:

IP标识全网唯一的一台主机。

Port标识该主机内唯一的一个网络进程。

IP+Port=全网内唯一的一个网络进程(Socket)。

当我们再发送数据时,必须带上源ip源port,以及目标ip目标port。

所以网络通信的本质:是全网唯二的两个网络进程,在进行进程间通信,相互都在用对方ip与port来标识对方的唯一性。

TCP/UDP协议:

TCP协议:

属于传输层协议

有连接:简单来说就是传输时候,必须与对方TCP建立连接

可靠传输:如果数据出现丢包,会给对方重新传输,如果传输过快,就传慢点,反正。

是面向字节流

UDP协议:

同属于传输层协议

无连接

不可靠传输:指只管传输,不管其他

面向数据报

UDP与TCP区别:

TCP协议与UDP协议的差别就是,TCP是对接的,UDP是广播的。所以在实现上TCP肯定比UDP更为复杂,更占用资源,如果使用场景是需要保证数据通常,丢包就要重新传的则使用TCP协议,如果只是想简单传输如广播,不在乎丢包则则使用UDP。

这里的可靠与不可靠,不能简单的从字面上进行理解,更多的要放到具体场景中选择使用。

粗略理解网络进程如何获取数据:

Linux下一切皆文件,那么网络也属于文件,那既然是文件,文件系统就要为该文件创建struct file对象,是struct file对象里就会有文件缓冲区,那么将有效载荷载入到文件缓冲区里,进程就能通过该网络文件缓冲区,把数据刷新到进程里就能获取数据。

大小端存储:

我们的机器有些是大端机,有些是小端机。大端机存储数据的格式是低地址存高权值位数据,而小端机则是低地址存低权值位的数据。

那我们为什么要提及这个内容呢?

小端机在发送数据时以小端存储格式进行发送,假设小端机将数据通过网络发送到大端机上,大端机进行解读时,就会以大端格式进行读取,就导致数据读反的问题。

因此网络协议规定,网络数据必须以大端格式进行传输。

在C库中有专门将数据数据转成网络数据函数,如果是小端存储就转成大端存存储格式,如果是大端存储则什么都不做。

Socket API接口:

除了第一个函数,会发现剩下四个函数都有 struct sockaddr结构体,那么这struct sockaddr究竟是什么呢。

struct sockaddr:

我们已经知道了网络通信的本质是进程间通信,在此之前我们已经学过了一个进程间通信的方法 基于system V标准的本地通信,而网络通信是 基于posix标准

而posxi虽是网络通信标准,但也能进行本地通信,因此socket有网络socket也有本地socket。所以socket会有很多种类来满足不同的场景,所以socket未来的接口就会有不同的通信接口规范。

而socket并不想那么复杂,他只想提供一种通用的通信接口。

sockaddr结构体分为两部分,第一部分是地址类型数据,第二部分是具体的地址数据,由 sa_family 决定解析方式(如 AF_INET 时按 sockaddr_in 解析,AF_UNIX 时按 sockaddr_un 解析)”。

Sockaddr_in属于网络通信的结构体,而sockaddr_un属于本地通信的结构体,而这两个都属于sockaddr的派生类(C语言实现),因此sockaddr既可以指向sockaddr_in也可以指向sockaddr_un。那么为了区分这两个类型,所以在16位地址类型in使用AF_INET标识网络通信协议,un使用本地通信协议。

我们知道网络进程通信需要包含端口号与IP地址,因此sockaddr_in里需要包含端口号与IP地址,而sockaddr_in最后八字节仅作为结构体内存对齐的填充位。

  函数int socket(int domain, int type, int protocol);

(1) domain:协议族 / 地址族

指定套接字使用的协议族(Address Family),决定通信的地址类型和协议范围。常见取值:

AF_INET:IPv4 协议族(最常用),地址格式为 IPv4 地址(如 192.168.1.1)。

AF_UNIX:本地进程间通信协议族

(2) type:套接字类型

指定套接字的数据传输语义,决定通信方式。常见取值:

SOCK_STREAM:流式套接字,提供面向连接、可靠、无边界的字节流(如 TCP)。

SOCK_DGRAM:数据报套接字,提供无连接、不可靠、有边界的数据包(如 UDP)。

SOCK_RAW:原始套接字,允许直接访问网络层协议(如 ICMP、自定义 IP 包,需 root 权限)。

SOCK_SEQPACKET:顺序包套接字,类似 SOCK_STREAM 但保留数据包边界(如 SCTP)。

(3) protocol:具体协议

指定套接字使用的具体协议,通常设为 0(由内核根据 domain 和 type 自动选择默认协议)

RETURN:

成功:返回一个非负整数(套接字描述符 sockfd),后续网络操作(如 bind、connect、send、recv)需通过该描述符进行。就像文件描述符一样,socket创建一个网络文件,返回值是该文件描述符。

失败:返回 -1,并设置全局变量 errno。

Socket项目:

1.简易英译中翻译器:

建立一个服务器程序与客户端程序,在服务器程序一开始启动时,通过词典的txt文件将所有中英文映射的单词载入内存,可以使用map进行存储,接着服务器接受客服端所发送的单词信息,解析出是哪个IP与哪个端口进行发送,并将通过回调函数,执行翻译程序,将客户端所发送的单词进行查找转化为中文,并返回给客户机。

UdpSever.hpp:
#pragma once// 包含必要的头文件#include <iostream>#include <string>#include <functional>  // 用于函数对象#include <strings.h>   // 用于bzero等函数#include <sys/types.h>#include <sys/socket.h> // 套接字相关函数#include <netinet/in.h> // 网络地址结构#include <arpa/inet.h>  // 字节序转换等函数#include"LogModel.hpp"  // 日志相关#include"IntAddr.hpp"   // 封装的地址类using namespace LogModel;// 定义函数对象类型:接收字符串和地址,返回处理后的字符串using func_t = std::function<std::string(const std::string&, IntAddr&)>;// 默认的无效文件描述符const int defaultfd = -1;// UDP服务器类,用于创建和运行一个UDP服务器class UdpServer{public:// 构造函数:初始化端口和处理数据的回调函数// port:服务器要绑定的端口号// func:用于处理收到的数据的回调函数UdpServer(uint16_t port, func_t func): _sockfd(defaultfd),  // 初始化套接字描述符为无效值_port(port),         // 记录端口号_isrunning(false),   // 服务器初始为未运行状态_func(func)          // 保存回调函数{}// 初始化服务器:创建套接字并绑定端口void Init(){// 创建UDP套接字(SOCK_DGRAM表示UDP)_sockfd = socket(AF_INET, SOCK_DGRAM, 0);if (_sockfd < 0)  // 套接字创建失败{LOG(LogLeve::FATAL) << "socket error!";  // 输出致命错误日志exit(1);  // 退出程序}LOG(LogLeve::INFO) << "socket success, sockfd : " << _sockfd;  // 输出成功日志// 初始化本地地址结构struct sockaddr_in local;bzero(&local, sizeof(local));  // 清空地址结构local.sin_family = AF_INET;    // 使用IPv4地址local.sin_port = htons(_port); // 端口号转换为网络字节序local.sin_addr.s_addr = INADDR_ANY;  // 绑定到所有本地网络接口// 将套接字与本地地址绑定int n = bind(_sockfd, (struct sockaddr *)&local, sizeof(local));if (n < 0)  // 绑定失败{LOG(LogLeve::FATAL) << "bind error";  // 输出致命错误日志exit(2);  // 退出程序}LOG(LogLeve::INFO) << "bind success, sockfd : " << _sockfd;  // 输出成功日志}// 启动服务器:循环接收数据并处理void Start(){_isrunning = true;  // 标记服务器为运行状态while (_isrunning)  // 服务器运行循环{char buffer[1024];  // 用于存储接收的数据struct sockaddr_in peer;  // 用于存储发送方的地址socklen_t len = sizeof(peer);  // 地址结构的长度// 接收数据:从套接字读取数据,同时获取发送方地址// buffer:存储数据的缓冲区// sizeof(buffer)-1:预留一个字节给结束符// 0:默认标志// &peer:发送方地址// &len:地址长度ssize_t s = recvfrom(_sockfd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&peer, &len);if (s > 0)  // 成功接收到数据{IntAddr addr(peer);  // 将地址结构封装为IntAddr对象buffer[s] = 0;       // 给字符串添加结束符// 调用回调函数处理数据,得到返回结果std::string result = _func(buffer, addr);// 将处理结果发送回客户端sendto(_sockfd, result.c_str(), result.size(), 0, (struct sockaddr*)&peer, len);}}}// 析构函数:目前为空,可根据需要添加资源释放逻辑~UdpServer(){}private:int _sockfd;         // 套接字描述符:用于标识服务器的套接字uint16_t _port;      // 服务器绑定的端口号bool _isrunning;     // 服务器运行状态:true表示运行中,false表示已停止func_t _func;        // 数据处理回调函数:用于自定义处理收到的数据};

这样有些细节需要注意,当创建本地sockaddr_in结构体时,需要将端口号从主机序列转换成网络序列,并且建议把服务器的IP地址设置为INADDR_ANY,这是一个宏值,但本质是一个0.0.0.0的IP地址,而为什么不写具体地址呢?

其目的在于这样做,就可以让该服务器主机上的所有IP都可以接受到客户端所发来的消息,只需要端口一样即可。如果想要客户端通过发送服务器所指定的IP地址,那么这里本地网络IP地址就指定填写。

IntAddr.hpp
#pragma once#include<iostream>#include <sys/socket.h>#include <netinet/in.h>  // 包含网络地址结构(sockaddr_in)定义#include <arpa/inet.h>   // 包含IP地址转换函数(inet_ntoa等)#include<string>// 封装网络地址信息的类,用于处理IP地址和端口号(网络字节序与本地字节序转换)class IntAddr{public:// 构造函数:通过sockaddr_in结构初始化地址信息// 参数addr:网络地址结构,包含IP、端口等信息(网络字节序)IntAddr(struct sockaddr_in &addr):_addr(addr)  // 保存原始的网络地址结构{// 将网络字节序的端口号转换为主机字节序并保存// 注意:此处原代码可能存在笔误,网络字节序转主机字节序应使用ntohs_port = htons(_addr.sin_port);  // htons是主机到网络,ntohs是网络到主机// 将网络地址结构中的IP地址(二进制)转换为字符串形式_ip = inet_ntoa(addr.sin_addr);}// 获取端口号(主机字节序)int Port(){return _port;}// 获取IP地址(字符串形式)std::string IP(){return _ip;}// 获取原始的网络地址结构(用于网络编程相关操作)const struct sockaddr_in &Addr(){return _addr;}// 生成格式化的地址字符串,格式如"[IP:端口]#"// 用于打印或显示地址信息std::string Input(){return "[" + _ip + ":" + std::to_string(_port) + "]#";}// 析构函数:空实现(无动态资源需要释放)~IntAddr(){}private:struct sockaddr_in _addr;  // 原始网络地址结构(存储网络字节序的地址信息)uint16_t _port;            // 主机字节序的端口号std::string _ip;           // 字符串形式的IP地址};

UdpSever.cc
#include <iostream>#include <memory>       // 用于智能指针(std::unique_ptr)#include"Dic.hpp"      // 字典类,提供翻译功能#include"UdpSever.hpp" // UDP服务器类,提供网络通信功能// 主函数:程序入口int main(int argc, char *argv[]){// 检查命令行参数是否正确:需要传入一个端口号作为参数if(argc != 2){// 如果参数错误,输出正确用法提示(程序名 + 端口号)std::cerr << "Usage: " << argv[0] << " port" << std::endl;return 1; // 异常退出}// 将命令行参数中的端口号(字符串)转换为无符号16位整数uint16_t port = std::stoi(argv[1]);// 1. 创建字典对象并加载词典数据,用于提供翻译功能Dic dict;dict.Load(); // 加载词典(可能从文件或其他数据源加载翻译数据)// 2. 创建UDP服务器智能指针(使用unique_ptr自动管理内存,避免内存泄漏)// 服务器绑定到指定端口,同时注册一个回调函数处理收到的消息std::unique_ptr usvr = std::make_unique<UdpServer>(port,  // 服务器绑定的端口号// 匿名回调函数:接收客户端消息和地址,返回处理结果[&dict](const std::string& message, IntAddr &addr){// 调用字典的翻译方法处理消息,返回翻译结果// 消息处理逻辑由Dic类的Translate方法实现return dict.Translate(message, addr);});// 初始化服务器:创建套接字、绑定端口等网络初始化操作usvr->Init();// 启动服务器:进入循环,持续接收客户端消息并通过回调函数处理后返回结果usvr->Start();return 0; // 正常退出}

dictionary.txt
apple: 苹果banana: 香蕉cat: 猫dog: 狗book: 书pen: 笔happy: 快乐的sad: 悲伤的hello:: 你好run: 跑jump: 跳teacher: 老师student: 学生car: 汽车bus: 公交车love: 爱hate: 恨hello: 你好goodbye: 再见summer: 夏天winter: 冬天

Dic.hpp
#include<iostream>#include<unordered_map>  // 用于哈希表存储,实现快速查询#include<string>#include<fstream>       // 用于文件读写操作#include"LogModel.hpp"  // 日志相关功能#include"IntAddr.hpp"   // 网络地址处理类// 全局变量:英文和中文的分隔符,用于解析词典文件std::string g_sep(": ");using namespace LogModel;// 全局变量:默认的词典文件路径std::string g_path="./dictionary.txt";// 字典类:用于加载英汉词典数据并提供翻译功能class Dic{public:// 构造函数:初始化词典文件路径,默认使用全局路径g_path// 参数path:词典文件的路径Dic(std::string path=g_path):_path(path)  // 初始化成员变量_path{}// 加载词典:从文件中读取数据并存储到哈希表中void Load(){// 打开词典文件std::ifstream file(_path);if(!file.is_open())  // 检查文件是否成功打开{LOG(LogLeve::DEBUG) << "打开字典: " << _path << " 错误";  // 记录调试日志return;}LOG(LogLeve::DEBUG)<<"Open file success";  // 记录文件打开成功的调试日志std::string line;  // 用于存储读取的每一行数据// 循环读取文件中的每一行while(getline(file,line)){// 在当前行中查找分隔符g_sep的位置auto it=line.find(g_sep);if(it==std::string::npos)  // 如果没找到分隔符,说明格式错误{LOG(LogLeve::WARING)<<"解析"<<line<<"失败";  // 记录警告日志continue;  // 跳过当前行,处理下一行}// 从分隔符位置拆分出英文单词和中文翻译std::string English=line.substr(0,it);  // 分隔符前的部分作为英文单词std::string Chinese=line.substr(it+g_sep.size());  // 分隔符后的部分作为中文翻译// 检查拆分后的英文或中文是否为空(数据不完整)if(English.empty()||Chinese.empty()){LOG(LogLeve::WARING)<<line<<"数据残缺";  // 记录警告日志continue;  // 跳过当前行}// 将有效的英汉对照数据存入哈希表_dic.insert(std::make_pair(English,Chinese));LOG(LogLeve::INFO)<<"加载数据"<<line;  // 记录信息日志,说明成功加载数据}file.close();  // 关闭文件}// 翻译功能:根据输入的英文单词查找对应的中文翻译// 参数message:要翻译的英文单词// 参数addr:发送请求的客户端地址(用于日志记录)// 返回值:对应的中文翻译,若未找到则返回"None"std::string Translate(const std::string &message, IntAddr& addr ){// 在哈希表中查找英文单词auto it=_dic.find(message);if(it==_dic.end())  // 未找到对应的翻译{LOG(LogLeve::INFO)<<addr.Input()<<"None";  // 记录日志:客户端地址 + 未找到return "None";  // 返回"None"表示未找到}// 找到翻译,记录日志并返回中文结果LOG(LogLeve::INFO)<<addr.Input()<<it->second;  // 日志:客户端地址 + 中文翻译return it->second;  // 返回对应的中文翻译}// 析构函数:空实现(无动态分配资源需要释放)~Dic(){}private:std::string _path;  // 词典文件的路径// 哈希表:key是英文单词,value是对应的中文翻译,用于快速查询std::unordered_map<std::string,std::string>_dic;};

UdpClien.cc:
#include <iostream>#include <string>#include <cstring>  // 用于内存初始化函数(如memset)#include <netinet/in.h>  // 包含网络地址结构定义#include <arpa/inet.h>   // 包含IP地址和端口转换函数#include <sys/types.h>#include <sys/socket.h>  // 包含套接字操作函数// 程序运行方式:./udpclient server_ip server_port(需要传入服务器IP和端口)int main(int argc, char *argv[]){// 检查命令行参数是否正确:必须传入服务器IP和端口两个参数if (argc != 3){// 输出正确用法提示(程序名 + 服务器IP + 服务器端口)std::cerr << "Usage: " << argv[0] << " server_ip server_port" << std::endl;return 1;  // 参数错误,退出程序}// 从命令行参数中提取服务器IP和端口std::string server_ip = argv[1];  // 服务器IP地址uint16_t server_port = std::stoi(argv[2]);  // 服务器端口号(字符串转数字)// 1. 创建UDP套接字// AF_INET:使用IPv4协议;SOCK_DGRAM:使用UDP协议;0:默认协议int sockfd = socket(AF_INET, SOCK_DGRAM, 0);if(sockfd < 0)  // 套接字创建失败{std::cerr << "socket error" << std::endl;  // 输出错误信息return 2;  // 创建失败,退出程序}// 初始化服务器地址结构(用于指定数据发送的目标)struct sockaddr_in server;  // 存储服务器网络地址信息memset(&server, 0, sizeof(server));  // 清空地址结构server.sin_family = AF_INET;  // 使用IPv4地址族server.sin_port = htons(server_port);  // 端口号转换为网络字节序(主机到网络)// IP地址转换为网络字节序(字符串转二进制)server.sin_addr.s_addr = inet_addr(server_ip.c_str());// 无限循环:持续与服务器通信while(true){std::string input;  // 存储用户输入的内容std::cout << "Please Enter# ";  // 提示用户输入std::getline(std::cin, input);  // 获取用户输入的一行字符串// 发送数据到服务器// 参数:套接字描述符、发送内容、内容长度、默认标志、服务器地址、地址长度int n = sendto(sockfd, input.c_str(), input.size(), 0, (struct sockaddr*)&server, sizeof(server));(void)n;  // 忽略返回值(实际可判断发送是否成功)// 接收服务器的响应char buffer[1024];  // 存储接收数据的缓冲区struct sockaddr_in peer;  // 存储发送方(服务器)的地址信息socklen_t len = sizeof(peer);  // 地址结构长度// 接收数据:参数同sendto,从服务器接收数据到bufferint m = recvfrom(sockfd, buffer, sizeof(buffer)-1, 0, (struct sockaddr*)&peer, &len);if(m > 0)  // 成功接收到数据{buffer[m] = 0;  // 添加字符串结束符(确保打印正常)std::cout << buffer << std::endl;  // 打印服务器返回的结果}}return 0;  // 程序正常退出(实际循环不会执行到这里)}

测试结果:

2.简单聊天室

设计一个简单的聊天室,通过多个用户拿着服务器的ip地址与端口号,往服务器进行发送消息,再通过服务器接受消息后,回调服务器里的Route路由功能函数,将消息转发给当前正在运行客户端程序的所有进程。

InAddr.hpp
#pragma once  // 防止头文件被重复包含#include<iostream>       // 标准输入输出流库#include <sys/socket.h>  // 包含socket相关函数定义#include <netinet/in.h>  // 包含IPv4地址结构等网络相关定义#include <arpa/inet.h>   // 包含IP地址转换函数(如inet_ntoa)#include<string>         // 字符串处理库// 网络地址转换及封装类,用于将网络字节序的地址转换为本地字节序并提供便捷操作class IntAddr{public:// 构造函数:接收网络地址结构sockaddr_in,进行字节序转换// 参数addr:网络字节序的sockaddr_in地址结构IntAddr(struct sockaddr_in &addr):_addr(addr)  // 初始化成员变量_addr{// 将网络字节序的端口号转换为主机字节序(htons:主机到网络,这里实际是网络到主机,原代码可能笔误,应为ntohs)_port=htons( _addr.sin_port);// 将网络字节序的IP地址转换为字符串形式(inet_ntoa:网络地址转ASCII字符串)_ip=inet_ntoa(addr.sin_addr);}// 获取主机字节序的端口号int Port(){return _port;}// 获取IP地址的字符串形式std::string IP(){return _ip;}// 获取原始的sockaddr_in地址结构(网络字节序)const struct sockaddr_in &Addr(){return _addr;}// 格式化输出IP和端口,返回如"[192.168.1.1 : 8080]"的字符串std::string Input(){return "["+_ip+" :"+std::to_string(_port)+"]";}// 重载==运算符,判断两个网络地址是否相同(IP和端口均相同)bool operator ==(const IntAddr&peer){return _ip==peer._ip&&_port==peer._port;}// 析构函数(空实现,无动态资源需要释放)~IntAddr(){}private:struct sockaddr_in _addr;  // 存储原始的网络字节序地址结构uint16_t _port;            // 主机字节序的端口号std::string _ip;           // IP地址的字符串形式};

Route.hpp
#pragma once  // 防止头文件重复包含#include<iostream>       // 标准输入输出流#include"IntAddr.hpp"    // 包含网络地址处理类的定义#include<vector>         // 用于存储在线成员的容器#include<string>         // 字符串处理#include"LogModel.hpp"   // 日志记录相关功能using namespace LogModel;  // 使用日志模型的命名空间// 路由管理类:负责管理在线成员、处理消息转发和成员上下线class Route{public:// 构造函数:初始化路由管理器Route(){}// 检查指定成员是否在线// 参数peer:要检查的成员地址// 返回值:存在(在线)返回true,否则返回falsebool Exist( IntAddr&peer){// 遍历在线成员列表,查找是否存在该成员for (auto &e:_online_member){if(e==peer)  // 利用IntAddr重载的==运算符判断地址是否相同return true;}return false;}// 添加成员到在线列表// 参数peer:要添加的成员地址void Add( IntAddr&peer){_online_member.push_back(peer);  // 将成员添加到在线列表// 记录INFO级日志,提示新增在线用户LOG(LogLeve::INFO)<<"新增一个在线用户"<<peer.Input();}// 从在线列表移除成员(处理退出)// 参数peer:要移除的成员地址void Quit(IntAddr&peer){// 遍历在线列表查找目标成员for (auto it=_online_member.begin();it!=_online_member.end();++it){if(*it==peer)  // 找到目标成员{// 记录INFO级日志,提示用户退出LOG(LogLeve::INFO)<<"在线用户"<<peer.Input()<<"退出";_online_member.erase(it);  // 从列表中删除该成员break;  // 退出循环,避免迭代器失效}}}// 消息转发:将消息发送给所有在线成员// 参数socketfd:用于发送消息的socket文件描述符// 参数message:要转发的消息内容// 参数peer:发送消息的源成员地址void Forward(int socketfd, const std::string&message, IntAddr&peer){// 如果发送者不在在线列表中,则添加进去if(!Exist(peer)){Add(peer);}// 构造转发消息:格式为"[IP:端口]#消息内容"std::string peer_massage;peer_massage+=peer.Input()+"#"+message;// 遍历所有在线成员,将消息发送给每个人(除了自己?当前代码会发给自己)for (auto &e:_online_member){// 使用sendto发送UDP消息(参数:socket、消息、长度、标志、目标地址、地址长度)int n=sendto(socketfd,peer_massage.c_str(),peer_massage.size(),0,(const struct sockaddr*)&e.Addr(),sizeof(e.Addr()));(void)n;  // 忽略发送结果(实际应用中应处理发送失败的情况)}// 如果消息是"QUIT",则将发送者从在线列表中移除if (message=="QUIT"){Quit(peer);}}// 析构函数:空实现(无动态分配资源需要释放)~Route(){}private:std::vector<IntAddr> _online_member;  // 存储所有在线成员的地址信息};
Thread.hpp
#pragma once#include <iostream>#include <string>#include <functional>#include <pthread.h>using func_t = std::function<void()>;static int ThreadID = 1;class Thread{void EnableRuing(){_IsRuing = true;}void EnableDecath(){_Isdecath = true;}public:Thread(func_t func): _pth(0), _Isdecath(false), _IsRuing(false), _func(func){_name = "thread-" + std::to_string(ThreadID++);}static void *Routine(void *args){Thread*th=static_cast<Thread*>(args);th->EnableRuing();if(th->_Isdecath==true){th->Decath();}pthread_setname_np(th->_pth,th->_name.c_str());th->_func();return nullptr;}bool Decath(){if(_Isdecath==true)return false;if(_IsRuing==true)pthread_detach(_pth);EnableDecath();return true;}bool Start(){if(_IsRuing)return false;int n=pthread_create(&_pth,nullptr,Routine,this);if(n!=0)return false;return true;}bool Stop(){if(_IsRuing){int n=pthread_cancel(_pth);if(n!=0){return false;}else{_IsRuing=false;return true;}}return false;}void Join(){if(_Isdecath){return;}int n=pthread_join(_pth,nullptr);(void)n;}pthread_t id(){return _pth;}std::string GetName(){return _name;}~Thread() {}private:pthread_t _pth;std::string _name;bool _Isdecath;bool _IsRuing;func_t _func;};
UdpServer.hpp
#pragma once  // 防止头文件重复包含#include <iostream>       // 标准输入输出流#include <string>         // 字符串处理#include <functional>     // 函数对象支持,用于定义回调函数类型#include <strings.h>      // 字符串操作函数(如bzero)#include <sys/types.h>    // 系统基本数据类型#include <sys/socket.h>   // socket相关函数(socket、bind、recvfrom等)#include <netinet/in.h>   // 网络地址结构定义(sockaddr_in等)#include <arpa/inet.h>    // IP地址转换函数#include "LogModel.hpp"   // 日志模块,用于记录运行信息#include "IntAddr.hpp"    // 网络地址封装类,处理IP和端口using namespace LogModel;  // 使用日志模块的命名空间// 定义回调函数类型:用于处理接收到的数据// 参数:socket描述符、接收的消息字符串、发送方地址对象using func_t = std::function<void(int socketfd, const std::string &, IntAddr &)>;// 默认的无效socket描述符const int defaultfd = -1;// UDP服务器类:封装UDP服务器的创建、初始化和数据接收逻辑class UdpServer{public:// 构造函数:初始化服务器端口和数据处理回调函数// 参数:port - 服务器监听的端口号;func - 处理接收数据的回调函数UdpServer(uint16_t port, func_t func): _sockfd(defaultfd),  // 初始化socket描述符为无效值_port(port),         // 保存服务器端口号_isrunning(false),   // 服务器初始为未运行状态_func(func)          // 保存数据处理回调函数{}// 初始化服务器:创建socket并绑定到指定端口void Init(){// 1. 创建UDP socket(IPv4协议,数据报服务)_sockfd = socket(AF_INET, SOCK_DGRAM, 0);if (_sockfd < 0)  // 检查socket创建是否失败{LOG(LogLeve::FATAL) << "socket error!";  // 记录致命错误日志exit(1);  // 退出程序}LOG(LogLeve::INFO) << "socket success, sockfd : " << _sockfd;  // 记录成功日志// 2. 准备本地地址结构(要绑定的IP和端口)struct sockaddr_in local;bzero(&local, sizeof(local));  // 清空地址结构local.sin_family = AF_INET;    // 使用IPv4地址族local.sin_port = htons(_port); // 端口号从主机字节序转为网络字节序local.sin_addr.s_addr = INADDR_ANY;  // 绑定到本机所有可用IP地址// 3. 将socket与本地地址绑定int n = bind(_sockfd, (struct sockaddr *)&local, sizeof(local));if (n < 0)  // 检查绑定是否失败{LOG(LogLeve::FATAL) << "bind error";  // 记录致命错误日志exit(2);  // 退出程序}LOG(LogLeve::INFO) << "bind success, sockfd : " << _sockfd;  // 记录成功日志}// 启动服务器:循环接收数据并调用回调函数处理void Start(){_isrunning = true;  // 标记服务器为运行状态while (_isrunning)  // 服务器运行循环{char buffer[1024];  // 接收数据的缓冲区struct sockaddr_in peer;  // 存储发送方(客户端)的地址信息socklen_t len = sizeof(peer);  // 地址结构的长度// 接收数据:从socket读取数据到缓冲区// 参数:socket描述符、缓冲区、最大长度、标志、发送方地址、地址长度ssize_t s = recvfrom(_sockfd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&peer, &len);if (s > 0)  // 接收成功{IntAddr addr(peer);  // 将发送方地址转换为IntAddr对象(便于处理)buffer[s] = 0;       // 给字符串添加终止符,确保打印和处理正确// 调用回调函数处理接收到的数据_func(_sockfd, buffer, addr);// 注:原注释中的sendto是示例,实际由回调函数决定如何处理(如转发、回复等)}}}// 析构函数:目前为空,若有动态资源可在此释放~UdpServer(){}private:int _sockfd;         // UDP socket的文件描述符uint16_t _port;      // 服务器监听的端口号bool _isrunning;     // 服务器运行状态标志(true表示运行中)func_t _func;        // 数据处理回调函数:由外部定义,用于处理接收到的消息};

UdpServer.cc
#include <iostream>#include <memory>#include"UdpSever.hpp"#include"Route.hpp"int main(int argc, char *argv[]){if(argc != 2){std::cerr << "Usage: " << argv[0] << " port" << std::endl;return 1;}// std::string ip = argv[1];uint16_t port = std::stoi(argv[1]);Route route;// 2. 网络服务器对象,提供通信功能std::unique_ptr usvr=std::make_unique<UdpServer>(port,[&route](int fd,const std::string&message,IntAddr &addr){route.Forward(fd,message,addr);});usvr->Init();usvr->Start();return 0;}

UdpeClient.cc

#include <iostream>       // 标准输入输出流,用于打印信息和接收用户输入#include <string>         // 字符串处理,用于存储IP、消息等#include <cstring>        // 字符串操作函数,如memset#include <netinet/in.h>   // 定义网络地址结构(如sockaddr_in)#include <arpa/inet.h>    // 提供IP地址转换函数(如inet_addr)#include <sys/types.h>    // 定义基本系统数据类型#include <sys/socket.h>   // 提供socket相关函数(如socket、sendto、recvfrom)#include"Thread.hpp"      // 自定义线程类头文件,用于多线程处理// 全局变量:存储服务器IP地址std::string server_ip;// 全局变量:存储服务器端口号uint16_t server_port;// 全局变量:接收线程的ID,用于后续取消线程pthread_t id;// 全局变量:UDP socket的文件描述符,用于网络通信int sockfd;// 接收消息线程函数:循环接收服务器转发的消息并打印void Recv(){while (true)  // 无限循环,持续接收消息{char buffer[1024];  // 缓冲区,用于存储接收的消息struct sockaddr_in peer;  // 存储发送方(服务器或其他客户端)的地址信息socklen_t len = sizeof(peer);  // 地址结构的长度// 接收数据:从sockfd接收消息到buffer,最多接收1023字节(留一个位置给终止符)// 参数:socket描述符、缓冲区、长度、标志、发送方地址、地址长度int m = recvfrom(sockfd, buffer, sizeof(buffer)-1, 0, (struct sockaddr*)&peer, &len);if(m > 0)  // 接收成功{buffer[m] = 0;  // 给字符串添加终止符,确保打印正确std::cerr << buffer << std::endl;  // 打印接收的消息(用cerr避免与输入提示混在一起)}}}// 发送消息线程函数:循环读取用户输入并发送给服务器void Send(){struct sockaddr_in server;  // 服务器的地址结构memset(&server, 0, sizeof(server));  // 初始化地址结构,清零server.sin_family = AF_INET;  // 设置地址族为IPv4server.sin_port = htons(server_port);  // 将端口号从主机字节序转为网络字节序server.sin_addr.s_addr = inet_addr(server_ip.c_str());  // 将IP字符串转为网络字节序的整数while(true)  // 无限循环,持续等待用户输入{std::string input;  // 存储用户输入的消息std::cout << "Please Enter# ";  // 提示用户输入std::getline(std::cin, input);  // 读取用户输入的一行内容// 发送数据:将input发送到服务器地址// 参数:socket描述符、消息内容、长度、标志、目标地址、地址长度int n = sendto(sockfd, input.c_str(), input.size(), 0, (struct sockaddr*)&server, sizeof(server));(void)n;  // 忽略发送结果(实际应用中应检查是否发送成功)if(input=="QUIT")  // 如果用户输入"QUIT",则退出程序{pthread_cancel(id);  // 取消接收线程break;  // 退出发送循环}}}// 程序入口:用法 ./udpclient server_ip server_portint main(int argc, char *argv[]){// 检查命令行参数:必须传入服务器IP和端口号两个参数if (argc != 3){std::cerr << "Usage: " << argv[0] << " server_ip server_port" << std::endl;return 1;  // 参数错误,退出程序}// 解析命令行参数:服务器IP和端口号server_ip = argv[1];server_port = std::stoi(argv[2]);  // 将字符串端口号转为整数// 1. 创建UDP socket:使用IPv4协议(AF_INET),UDP类型(SOCK_DGRAM)sockfd = socket(AF_INET, SOCK_DGRAM, 0);if(sockfd < 0)  // 检查socket创建是否成功{std::cerr << "socket error" << std::endl;return 2;  // 创建失败,退出程序}// 创建接收线程和发送线程(使用自定义Thread类)Thread recv(Recv);  // 接收线程,绑定Recv函数Thread send(Send);  // 发送线程,绑定Send函数// 启动线程:开始执行Recv和Send函数recv.Start();send.Start();// 记录接收线程的ID,用于后续在Send中取消该线程id = recv.id();// 等待线程结束:主线程阻塞,直到接收线程和发送线程执行完毕recv.Join();send.Join();return 0;  // 程序正常退出}

测试结果:

使用TCP协议进行编写词典翻译:

TCP属于有连接的协议,所以在利用TCP协议进行通信时,必须先让客户端与服务端建立连接。

int listen(int sockfd, int backlog)函数:

在 Linux 网络编程中,listen 是一个核心的系统调用,主要用于 TCP 服务器端,其作用是将一个已绑定(bind)的主动套接字(active socket)转换为被动套接字(passive socket),使其能够监听客户端的连接请求,并维护连接请求队列。

简单来说,TCP协议就像店家开店时候,门店要求必须要有一个人在看店,因为你不知道什么时候会有客人来访,所以就必须持续监听,等待客户端建立连接。

参数backlog:

指定已完成三次握手但未被 accept 处理的连接队列(completed connection queue)的最大长度。

返回值:

返回 0,表示套接字已进入监听状态,可通过 accept 函数接收连接。失败则返回-1。

Comd.hpp
#pragma once#include<iostream>#include <sys/types.h> #include <sys/socket.h>enum Error{OK=0,SOCK_ERROR,BIND_ERROR,LISTEN_ERROR};class nocopy{public:nocopy(){}~nocopy(){}nocopy(const nocopy&)=default;nocopy&operator=(nocopy&)=default;};#define COV(addr) ((struct sockaddr*)&addr)

TcpServer.hpp
#pragma once#include <iostream>#include "LogModel.hpp"#include "InetAddr.hpp"#include <functional>#include "Comd.hpp"#include "Thread.hpp"int ListenBlock = 5;using func_t = std::function<std::string(std::string,InetAddr &addr)>;std::string defaultip = "0.0.0.0";using namespace LogModel;class TcpServer : public nocopy{public:TcpServer(func_t func, uint16_t port, std::string ip = defaultip, int listenblock = ListenBlock): _func(func), _port(port), _ip(ip), _linset_sockfd(-1), _listenblock(listenblock){}void Init(){_linset_sockfd = socket(AF_INET, SOCK_STREAM, 0);if (_linset_sockfd < 0){LOG(LogLeve::FATAL) << "creat socket fail";exit(Error::SOCK_ERROR);}LOG(LogLeve::DEBUG) << "creat socket success";InetAddr addr(_port, _ip);int n = bind(_linset_sockfd, addr.NetAddrPtr(), addr.IntAddrLen());if (n < 0){LOG(LogLeve::FATAL) << "bind fail";exit(Error::BIND_ERROR);}LOG(LogLeve::DEBUG) << "bind success";int k = listen(_linset_sockfd, _listenblock);if (k < 0){LOG(LogLeve::FATAL) << "listen fail";exit(Error::LISTEN_ERROR);}LOG(LogLeve::DEBUG) << "listen success";}class ThreadData{public:ThreadData(InetAddr &addr, int sockid, TcpServer *tsp): _addr(addr), _sockid(sockid), _tsp(tsp){}public:InetAddr &_addr;int _sockid;TcpServer *_tsp;};void Server(int sockfd,InetAddr &addr){while (1){char buff[1024];ssize_t n = read(sockfd, buff, sizeof(buff) - 1);if (n > 0){buff[n]=0;LOG(LogLeve::DEBUG)<<addr.Input()<<"#"<<buff;std::string ret=_func(buff,addr);int n=write(sockfd,ret.c_str(),ret.size());(void)n;}else if(n==0){LOG(LogLeve::INFO)<<addr.Input()<<"::Quit";break;}else{LOG(LogLeve::FATAL)<<"Read Error";break;}}}static void *Routine(void *args){ThreadData *td = (ThreadData *)args;td->_tsp->Server(td->_sockid,td->_addr);return nullptr;}void Run(){if (_isruning)return;_isruning = true;while (_isruning){struct sockaddr_in peer;socklen_t len = sizeof(peer);int sockfd = accept(_linset_sockfd, (struct sockaddr *)&peer, &len);if (sockfd < 0){LOG(LogLeve::WARING) << "accept fail";continue;}InetAddr addr(peer);LOG(LogLeve::DEBUG)<<addr.Input()<<":online";ThreadData *data = new ThreadData(addr, sockfd, this);pthread_t tid;pthread_create(&tid, nullptr, Routine, (void *)data);}}~TcpServer(){}private:int _linset_sockfd;uint16_t _port;std::string _ip;int _listenblock;func_t _func;bool _isruning = false;};

TCP协议采用的是面向字节流的通信方式,所以在socket函数里,传入的是SOCK_STREAM。

后续的通信读写,也就如同文件操作一般,使用write与read进行读写。

TcpServer.hpp采用多线程的方式来编写Server服务,原因是因为,如果只用但线程的方式,那么就只允许单对单的模式进行操作,而当其他客户端想访问服务器的时候是访问不到的,因为服务器线程正在死循环执行Server函数。

并且在Run函数里,accept返回的是sockid,此时TCP服务里就有两个sockid,一个是Listenid,一个则是普通的sockid,那么这两个有什么区别呢?

Listenid是用于监听的套接字,普通的sockid是类似文件描述符那般,用于进行读写。简单来说TCP就是一个饭店,Listenid就像在饭店门口揽客的员工,它只管进行揽客,当揽完客后,客人进到饭店里,后续的服务接待都是由普通的sockid员工进行。

TcpServer.cc:
#include"TcpServer.hpp"#include<memory>#include"Dic.hpp"int main(int argc, char *argv[]){if(argc != 2){std::cerr << "Usage: " << argv[0] << " port" << std::endl;return 1;}uint16_t port = std::stoi(argv[1]);Dic dic;dic.Load();std::unique_ptr<TcpServer> ptr=std::make_unique<TcpServer>([&dic](std::string message,IntAddr&peer){return dic.Translate(message,peer);},port);ptr->Init();ptr->Run();return 0;}

TCPClient.hpp
#include<iostream>#include"Common.hpp"#include<string.h>#include <unistd.h>#include"IntAddr.hpp"void  EchoClientError(char *argv[]){std::cerr<<argv[0]<<" ip prot"<<std::endl;}int main(int argc ,char *argv[]){if(argc!=3){EchoClientError(argv);exit(Error::START_ERROR);}std::string ip=argv[1];uint16_t port= std::stoi((argv[2]));IntAddr server(port,ip);int socketid=socket(AF_INET,SOCK_STREAM,0);if(socketid<0){std::cerr<<"socket creat fail"<<std::endl;exit(Error::SOCK_ERROR);}std::cout<<"socket success"<<std::endl;int n=connect(socketid,server.NetAddrPtr(),server.IntAddrLen());if(n<0){std::cerr<<"connect fail"<<std::endl;exit(Error::CONNECT_ERROR);}std::cout<<"connect success"<<std::endl;while (true){std::string line;std::cout << "Please Enter: ";std::getline(std::cin, line);write(socketid,line.c_str(),line.size());char buff[1024];int n=read(socketid,buff,sizeof(buff)-1);if(n>0){buff[n]=0;std::cout << "server echo# " << buff << std::endl;}}close(socketid);return 0;}

运行效果:

netstat -natp 是 Linux 系统中用于查看网络连接状态的常用命令,各个选项的含义及整体功能如下:

选项拆解:

n:--numeric,以数字形式显示 IP 地址和端口号(而非解析为主机名或服务名,如 80 不会显示为 http),速度更快且信息更直接。

a:--all,显示所有连接状态的套接字,包括:

正在监听的(LISTEN 状态,如服务器等待连接的端口);

已建立的(ESTABLISHED 状态,如已连接的通信);

处于其他状态的(如 TIME_WAIT、SYN_SENT 等)。

t:--tcp,仅显示TCP 协议相关的连接(排除 UDP、ICMP 等其他协议)。

p:--program,显示每个连接对应的进程 ID(PID)和进程名,方便定位哪个程序在占用网络资源(需要 root 权限才能查看所有进程信息,普通用户可能只能看到自己的进程)。

       本篇文章就介绍到这里,感谢各位观看!!!

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

相关文章:

  • 深入了解linux网络—— 网络编程基础
  • 焦作做网站哪家好提供微网站制作电话
  • 【嘉力创】天线阻抗设计
  • xlsx-js-style 操作 Excel 文件样式
  • 岛屿数量(广搜)
  • 美食网站要怎么做一个网站交互怎么做的
  • AppInventor2 使用 SQLite(二)导入外部库文件
  • AppGallery Connect(Harmony0S 5及以上)--公开测试流程
  • 深入解析:使用递归计算整数幂的C语言实现
  • 虚幻引擎入门教程开关门
  • 设计模式-组合模式详解
  • 什么是B域?
  • Android 用java程序模拟binder buffer的分配释放以及buffer的向前和向后合并
  • 专门做护肤品网站浙江立鹏建设有限公司网站
  • 电商会学着做网站呢设计师接单渠道
  • Postman 学习笔记 II:测试、断言与变量管理
  • electron设置默认应用程序
  • Flink 初体验10 分钟完成下载、安装、本地集群启动与示例作业运行
  • toLua[二] Examples 01_HelloWorld分析
  • asp源码打开网站网站页面数量
  • 安卓手机termux安装ubuntu被kill进程解决
  • java后端工程师进修ing(研一版‖day48)
  • 目标检测进化史
  • 北京做养生SPA的网站建设高端网站建设 来磐石网络
  • 网站建设有哪三部来年做那些网站能致富
  • 外贸公司网站素材产品营销文案
  • VSCode C/C++ 开发环境配置
  • FPGA自学笔记--VIVADO RAM IP核控制和使用
  • 电源——设计DCDC原理图与参数选型
  • 企业网站建设策划书 前言263云通信官方网站