Linux----Socket实现UDP简单服务器与客户端程序

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_INET IPv4
SOCK_DGRAM UDP
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_family IPv4
sin_port 本地端口(主机序 → 网络序)
sin_addr IP(字符串 → 网络序)

步骤 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);

这一行干了三件事:

  1. 收到 客户端发来的 UDP 数据

  2. 得到:

    • 数据内容

    • 客户端 IP + 端口(client)

  3. UDP 是无连接的,每次都要带 client 地址,这是 UDP 和 TCP 的核心区别之一


调用业务逻辑(回调)

复制代码
std::string echo_string = func(info);

这里:

  • info:客户端发来的字符串

  • funcExcuteCommand

  • 返回值:命令执行结果

网络层和业务层解耦


回包给客户端

复制代码
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 访问自己:

需要注意的地方:

  1. 使用本机回环通信(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 无法访问的现象。这种情况并不是程序逻辑错误,而是云服务器网络安全策略的必然结果

  2. 为什么客户端使用 127.0.0.1 可以通信,而使用云服务器的 IP 却不行?

    127.0.0.1 是回环地址,永远指向当前主机自身,当客户端和服务器在同一台机器上时,数据在内核中直接完成投递,不经过真实网络;而云服务器 IP 表示一台真实存在于网络中的远程主机,只有当该主机真实存在、网络可达并且对应端口有进程监听时,客户端发送的数据才能被接收,否则数据按照路由表发往公网后无人处理,自然得不到响应。

  3. 为什么客户端不能把 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_aton inet_pton inet_ntop htons htonl ntohs ntohl
典型使用位置 sin_addr sin_port
是否可直接用于 socket 是(需配合使用)
相关推荐
A小辣椒18 小时前
TShark:Wireshark CLI 功能
linux
A小辣椒1 天前
TShark:基础知识
linux
AlfredZhao1 天前
OCI 明明分配了 200G 系统盘,为什么 df 只看到 30G?
linux·oci
AlfredZhao2 天前
vi 删除指定范围的行,不用再反复按 dd
linux·vi
用户9718356334662 天前
银河麒麟 KY10 申威(SW64) 安装 nginx-1.16.1-2.p01.ky10.sw_64.rpm 详细步骤
linux
猪脚踏浪2 天前
linux 拷贝文件或目录到指定的位置
linux
大树883 天前
金刚石散热越强,管路越先见顶
大数据·运维·服务器·人工智能·ai
摇滚侠3 天前
Linux CentOS7 rpm 安装 MySQL 5.7
linux·运维·mysql
霸道流氓气质3 天前
领域驱动设计(DDD)在 Spring Boot 微服务中的实践指南
运维·spring boot·微服务
bush43 天前
嵌入式linux学习记录十四、术语
linux·嵌入式