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 是(需配合使用)
相关推荐
CoderIsArt14 小时前
iSCSI架构中客户端与服务端
服务器·网络·架构
EllenShen12314 小时前
服务器检测databricks job的运行状态封装
运维·azure
TPBoreas14 小时前
服务器CPU过高问题排查思路
运维·服务器
信创天地14 小时前
信创环境下CI/CD与灾备体系构建:从异构挑战到自主可控的运维革命
运维·ci/cd
h7ml14 小时前
企业微信外部联系人同步中的数据一致性与最终一致性保障
运维·服务器·企业微信
love530love14 小时前
EPGF 新手教程 04一个项目一个环境:PyCharm 是如何帮你“自动隔离”的?(全 GUI,新手零命令)
运维·开发语言·ide·人工智能·python·pycharm
oMcLin14 小时前
如何在Ubuntu 22.04上通过配置LVM优化存储,提升香港服务器的大规模数据库的读写性能?
服务器·数据库·ubuntu
默|笙14 小时前
【Linux】进程控制(4)自主shell命令行解释器
linux·运维·chrome
草莓熊Lotso14 小时前
从冯诺依曼到操作系统:打通 Linux 底层核心逻辑
linux·服务器·c++·人工智能·后端·系统架构·系统安全