Linux----Socket编程基础-CSDN博客
https://blog.csdn.net/weixin_60720508/article/details/156490358这里简单使用UDP实现一个小程序。因为UDP是面向数据报的,所以不能直接使用read(),write()来读写数据。而是要使用recvfrom(),sendto()来读写数据。
recvfrom
cpp#include<sys/types.h> #include<sys/socket.h> //函数原型 ssize_t recvform(int sockfd,void *buf,size_t len, int flags,struct socket *src_addr, socklen_t *addrlen);ssize_t 相当于 int ,socklen_t 相当于 int
参数说明:
socket:套接字
buf:缓冲区
len:缓冲区的长度
flags:调用操作方式,是以下一个或多个标志的组合,可通过or操作连接在一起,默认为0
src_addr:指针,指向装有源地址的缓冲区
addrlen:指针,指向from缓冲区长度值
返回值:
如果成功返回读取的字节数,如果失败,返回-1
如果远端关闭了文件描述符,返回0;
sendto
cpp#include<sys/types.h> #include<sys/socket.h> //函数原型 ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,struct sockaddr *dest_addr, socklen_t addrlen);参数说明:
socket:套接字
buff:待发送数据的缓冲区
size:缓冲区长度
flags:调用方式标志位,一般为0
dest_addr: 指针,指向目的套接字的地址
addrlen:所指地址的长度
返回值:成功返回发送的字节数,失败返回-1
实现代码
udp服务器端
cpp//UdpServer.hpp #pragma once #include <string> #include <sys/types.h> /* See NOTES */ #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #include<string.h> #include<iostream> #include<functional> #include <unistd.h> uint16_t defaultport = 8080; std::string defaultip = "0.0.0.0"; const int size = 1024; using func_t=std::function<std::string(const std::string&)>; class UdpServer { public: UdpServer(const uint16_t &port = defaultport, const std::string &ip = defaultip) : sockfd_(0), ip_(ip), port_(port), isrunning_(false) { } void Init() { //1. 创建udp socket sockfd_=socket(AF_INET, SOCK_DGRAM,0); if(sockfd_<0) exit(-1); //bind socket struct sockaddr_in local; bzero(&local,sizeof(local)); local.sin_family=AF_INET; local.sin_port=htons(port_); local.sin_addr.s_addr=inet_addr(ip_.c_str());//1.string->uint32_t if(bind(sockfd_,(const struct sockaddr*)&local,sizeof(local))<0) exit(-2); } void Run(func_t func) { isrunning_=true; char inbuffer[size]; while(isrunning_) { struct sockaddr_in client; socklen_t len=sizeof(client); ssize_t n=recvfrom(sockfd_,inbuffer,sizeof(inbuffer)-1,0,(struct sockaddr*)&client,&len); if(n<0) std::cout<<"warning"<<std::endl; inbuffer[n]=0; std::string info=inbuffer; std::string echo_string=func(info); sendto(sockfd_,echo_string.c_str(),echo_string.size(),0,(const sockaddr*)&client,len); } } ~UdpServer(){ if(sockfd_>0) close(sockfd_); } private: int sockfd_; // 网络文件描述符 std::string ip_; // 任意地址bind 0 uint16_t port_; // 服务器进程的端口号 bool isrunning_; };
cpp#include"UdpServer.hpp"" #include<memory> void usage(const std::string& str) { std::cout<<"\n\rUsage:"<<str<<"port[1024+]\n"<<std::endl; } std::string ExcuteCommand(const std::string &cmd) { FILE* fp=popen(cmd.c_str(),"r");//stdout----->fp if(fp==nullptr){ perror("popen"); return "error"; } std::string result; char buffer[1024]; while(true) { char* ok=fgets(buffer,sizeof(buffer),fp); if(ok==nullptr) break; result+=buffer; } pclose(fp); return result; } int main(int argc, char* argv[]) { if(argc!=2) { usage(argv[0]); exit(0); } std::unique_ptr<UdpServer> ptr(new UdpServer()); ptr->Init(); ptr->Run(ExcuteCommand); return 0; }这是一个 UDP 服务器,它接收客户端发来的字符串,把字符串当成 shell 命令在服务器上执行,然后把执行结果通过 UDP 原样返回给客户端。
也就是说:
客户端发: "ls" 服务器做: popen("ls") 服务器回: ls 的输出成员变量的含义
int sockfd_; // UDP socket std::string ip_; // 绑定 IP uint16_t port_; // 绑定端口 bool isrunning_; // 服务器是否运行本质:封装了一个 UDP socket 的生命周期
构造函数
UdpServer(const uint16_t &port = defaultport, const std::string &ip = defaultip)
默认监听:
IP:
0.0.0.0(所有网卡)Port:
8080并没有真正创建 socket
只是保存参数
Init() ------ 网络初始化阶段
步骤 1:创建 UDP socket
sockfd_ = socket(AF_INET, SOCK_DGRAM, 0);
参数 含义 AF_INETIPv4 SOCK_DGRAMUDP 0默认协议 此时只是拿到一个"通信句柄"
步骤 2:准备服务器地址结构
struct sockaddr_in local; bzero(&local, sizeof(local));初始化结构体:
local = { sin_family = AF_INET sin_port = ? sin_addr = ? }
步骤 3:填充地址信息
local.sin_family = AF_INET; local.sin_port = htons(port_); local.sin_addr.s_addr = inet_addr(ip_.c_str());这一步非常关键:
字段 说明 sin_familyIPv4 sin_port本地端口(主机序 → 网络序) sin_addrIP(字符串 → 网络序)
步骤 4:bind ------ "占住端口"
bind(sockfd_, (struct sockaddr*)&local, sizeof(local));"这个 UDP socket 负责接收发往 IP:port 的数据包"
Run(func_t func) ------ 核心循环
using func_t = std::function<std::string(const std::string&)>;UdpServer 不关心业务逻辑,只负责网络通信
while 循环:UDP 服务主循环
while (isrunning_)服务器一旦启动,就不停收包。
接收客户端数据
recvfrom(sockfd_, inbuffer, sizeof(inbuffer)-1, 0, (struct sockaddr*)&client, &len);这一行干了三件事:
收到 客户端发来的 UDP 数据
得到:
数据内容
客户端 IP + 端口(client)
UDP 是无连接的,每次都要带 client 地址,这是 UDP 和 TCP 的核心区别之一
调用业务逻辑(回调)
std::string echo_string = func(info);这里:
info:客户端发来的字符串
func:ExcuteCommand返回值:命令执行结果
网络层和业务层解耦
回包给客户端
sendto(sockfd_, echo_string.c_str(), echo_string.size(), 0, (struct sockaddr*)&client, len);把结果发回刚刚发请求的那个客户端。
ExcuteCommand 做了什么?
FILE* fp = popen(cmd.c_str(), "r");这一步:
启动
/bin/sh -c cmd把命令的 stdout 重定向到 fp
然后:
fgets → result += buffer把命令输出完整读回来,作为字符串返回
客户端
参数解析 & usage
if(argc != 3)要求启动方式:
./udpclient serverip serverport比如:
./udpclient 127.0.0.1 8080保存服务器地址信息
std::string serverip = argv[1]; uint16_t serverport = std::stoi(argv[2]);客户端必须知道服务器在哪里 ,但:客户端不需要 bind 自己的 IP 和端口
构造服务器 sockaddr_in
struct sockaddr_in server; bzero(&server,sizeof(server)); server.sin_family = AF_INET; server.sin_port = htons(serverport); server.sin_addr.s_addr = inet_addr(serverip.c_str());这一步本质是:
告诉内核:我要把 UDP 数据发到这个 IP + Port
创建 UDP socket
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);含义:
IPv4
UDP
一个"通信句柄"
这里没有 bind,关键概念:客户端的「隐式绑定」
// 客户端 implicit bind(隐式绑定)什么是隐式绑定?
当客户端:
sendto(sockfd, ...)而 这个 socket 没有 bind 时,内核会自动帮你:
选一个本机 IP
分配一个 1024--65535 的临时端口
完成 bind
这就叫:
implicit bind(隐式绑定)
所以:服务器能看到客户端 IP + port,客户端自己不关心
主循环:客户端真正干活的地方
while(true)客户端是一个交互式程序。
读取用户输入
std::cout << "Please Enter@ "; getline(std::cin, message);用户输入一行字符串,例如:
ls pwd cd ..
发送 UDP 数据给服务器
sendto(sockfd, message.c_str(), message.size(), 0, (struct sockaddr*)&server, len);这一步干的事:
把用户输入的字符串,通过 UDP 发给服务器
UDP 特点:不建立连接,每次 sendto 都是一个完整数据报
接收服务器返回的数据
recvfrom(sockfd, buffer, 1023, 0, (struct sockaddr*)&temp, &len);这里:
buffer:服务器返回的数据
temp:真正回包的地址虽然你已经知道服务器是谁,但 UDP 仍然会返回对端地址
一次完整交互:
客户端: sendto("ls") -------------------> 服务器: recvfrom popen("ls") sendto("main.cc ...") 客户端: recvfrom print result客户端负责"输入和展示",服务器负责"执行和返回",UDP 只负责搬运字节
这个是本机回环通信:这个是使用云服务器公网 IP 访问自己:
需要注意的地方:
使用本机回环通信(127.0.0.1)与使用公网 IP 访问自己有什么区别?为什么在云服务器环境中,公网 IP 访问自己可能会被拒绝?应如何解决?
一、本机回环通信(127.0.0.1)与公网 IP 访问自己的本质区别
当客户端使用 127.0.0.1 访问服务器时,属于本机回环通信。此时数据包不会离开主机,不经过物理或虚拟网卡,也不会进入云平台的网络系统,而是由操作系统内核直接将数据从客户端进程投递给服务器进程。这种通信只依赖于本机是否有进程在对应端口上监听,与公网 IP、路由规则以及云安全组配置完全无关。
当客户端使用公网 IP(如 106.13.101.24)访问服务器时,即便客户端和服务器运行在同一台云服务器上,数据包也会被当作一次真实的网络通信来处理。数据需要经过虚拟网卡、云平台的虚拟交换网络,并且必须通过云安全组(云防火墙)的入站规则检查,之后才能被操作系统内核交付给目标 socket。
因此,两者的根本区别在于:使用 127.0.0.1 进行通信不走网络、不受云安全组限制,而使用公网 IP 进行访问必须完整经过云平台的网络路径和安全策略。
二、为什么云服务器中"使用公网 IP 访问自己"会被拒绝
在云服务器环境中,公网 IP 的入站流量默认是被拒绝的。云厂商通过安全组机制实现"默认拒绝、显式放行"的安全策略。若安全组中未放行对应协议和端口,或者安全组未绑定到该实例,又或者协议或端口不匹配,那么所有来自公网 IP 的访问请求(包括访问自己)都会在云平台层被直接丢弃,服务器进程根本无法接收到数据。
而本机回环通信绕过了云平台的网络与安全组机制,因此会出现 127.0.0.1 可以正常通信,而公网 IP 无法访问的现象。这种情况并不是程序逻辑错误,而是云服务器网络安全策略的必然结果
为什么客户端使用 127.0.0.1 可以通信,而使用云服务器的 IP 却不行?
127.0.0.1 是回环地址,永远指向当前主机自身,当客户端和服务器在同一台机器上时,数据在内核中直接完成投递,不经过真实网络;而云服务器 IP 表示一台真实存在于网络中的远程主机,只有当该主机真实存在、网络可达并且对应端口有进程监听时,客户端发送的数据才能被接收,否则数据按照路由表发往公网后无人处理,自然得不到响应。
为什么客户端不能把 0.0.0.0 作为目标 IP 地址?
0.0.0.0 只具有监听或"本地未指定"的语义,并不是一个可路由、可到达的网络地址。bind 使用 0.0.0.0 表示"在所有本地地址上接收数据",而 sendto 或 connect 必须指定一个明确的、可路由的目标主机地址,内核需要根据该地址查找路由、选择网卡并发送数据,0.0.0.0 无法完成这些步骤,因此不能作为客户端发送数据时的目标 IP。
inet_aton类函数与hton的区别
对比维度 inet_aton / inet_pton hton / ntoh 主要作用 IP 表示形式转换 整数的字节序转换 解决的问题 字符串 IP → 二进制 IP 主机序 ↔ 网络序 操作对象 IP 地址 端口号 / 数值 是否处理字节序 是(隐式完成) 是(显式完成) 常见函数 inet_atoninet_ptoninet_ntophtonshtonlntohsntohl典型使用位置 sin_addrsin_port是否可直接用于 socket 是 是(需配合使用)

