【计算机网络】简学深悟启示录:scoket编程之udp

文章目录

1.UDP建立

由于 UDP无连接不可靠面向数据报 」的特性,分服务端客户端 两步实现,核心是「套接字创建 + 数据收发

2.服务端

2.1 启动函数main

main.cpp:

cpp 复制代码
#include "UdpServer.hpp"

Log logger;

void Usage(const char *proc)
{
    std::cout << "\n\rUsage: " << proc << " port[1024+]\n" << std::endl;
}

int main(int argc, char *argv[]) 
{
    if(argc != 2)
    {
        Usage(argv[0]);
        exit(0);
    }

    uint16_t port = std::stoi(argv[1]);

    // 设置日志输出到屏幕
    logger.Enable(Screen);
    std::unique_ptr<UdpServer> svr(new UdpServer(port));
    svr->Init();
    svr->Run();
    return 0;
}

设置命令行参数,启动服务端的命令必须为两个,Usage 函数用于提示用户正确输入,一个是可执行文件 ./UdpServer,另一个是指定执行的端口(注意要使用 1024+ 的,因为 0~1024 的端口通常包含具体对应的端口,容易产生冲突),为什么不用设置 IP 地址呢?下面的服务端地址会解释,然后使用 unique_ptr 智能指针创建唯一的服务端对象,避免多端启动,同时能够自动回收资源,最后依次 Init 初始化,Run 启动

2.2 服务端创建

cpp 复制代码
#pragma once
#include <iostream>
#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <cstring>
#include <memory>
#include <string>
#include <errno.h>
#include "log.hpp"

extern Log logger;

enum
{
    SOCKET_ERROR = 1,
    BIND_ERROR = 2
};

uint16_t default_port = 8080;
std::string default_ip = "0.0.0.0";
const int size = 1024;

class UdpServer 
{
public:
    UdpServer(uint16_t port = default_port, const std::string &ip = default_ip)
    : port_(port)
    , ip_(ip)
    , sockfd_(0)
    {}

    void Init()
    {
        // 1.创建套接字
        sockfd_ = socket(AF_INET, SOCK_DGRAM, 0);
        if(sockfd_ < 0)
        {
            logger(Fatal, "socket create error, sockfd: %d", sockfd_);
            exit(SOCKET_ERROR);
        }
        logger(Info, "socket create success, sockfd: %d", sockfd_);

        // 2.绑定端口号和IP地址
        struct sockaddr_in local;
        memset(&local, 0, sizeof(local));
        local.sin_family = AF_INET;         
        local.sin_port = htons(port_); // 转换为网络字节序
        // local.sin_addr.s_addr = inet_addr(ip_.c_str()); // 转换为网络字节序
        local.sin_addr.s_addr = htonl(INADDR_ANY); // 绑定本机所有IP地址

        int n = bind(sockfd_, (struct sockaddr*)&local, sizeof(local));
        if(n < 0)
        {
            logger(Fatal, "socket bind error, errno: %d, err string: %s", errno, strerror(errno));
            exit(BIND_ERROR);
        }
        logger(Info, "socket bind success, port: %d, ip: %s", port_, ip_.c_str());
    }

    void Run()
    {
        isrunning_ = true;
        char inbuffer[size];
        while(true)
        {
            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)
            {
                logger(Warning, "recvfrom client data error, n: %ld, errno: %d, err string: %s", n, errno, strerror(errno));
                continue;
            }
            inbuffer[n] = 0;
            std::string info = inbuffer;
            std::string echo_string = "server echo# " + info;
            std::cout << echo_string << std::endl;
            sendto(sockfd_, echo_string.c_str(), echo_string.size(), 0, (struct sockaddr*)&client, len);
        }
    }

    ~UdpServer()
    {
        if(sockfd_ > 0)
        {
            close(sockfd_);
        }
    }
private:   
    int sockfd_;
    uint16_t port_;
    std::string ip_;
    bool isrunning_;
};

UdpServer 构造函数初始化文件描述符,端口和 IP 地址,创建套接字的过程其实就是创建一个文件,对于服务器来说,你所看到的 IP 其实是处理过的,不是实际 IP,云服务器禁止直接绑定公网 IP,但是内网 IP 可以

通常服务端绑定 0.0.0.0 表示接收所有客户端的请求,若指定具体 IP(如 192.168.1.100),则只有发往该 IP 的数据能被接收,其他 IP 会被丢弃;绑定 0.0.0.0 则兼容所有网卡,客户端无论从本机、内网、外网访问,都能连接到服务端,无需关心主机的具体 IP 配置

2.2.1 创建套接字

cpp 复制代码
sockfd_ = socket(AF_INET, SOCK_DGRAM, 0);
if(sockfd_ < 0)
{
    logger(Fatal, "socket create error, sockfd: %d", sockfd_);
    exit(SOCKET_ERROR);
}
logger(Info, "socket create success, sockfd: %d", sockfd_);

domain 表示协议族,最常见的就是 AF_INETAF_INET6,表示 IPV4IPV6 网络协议。type 表示套接字类型,决定了数据传输的方式,SOCK_STREAM 表示字节流,通常指 TCPSOCK_DGRAM 表示数据流,通常指 UDPprotocol 表示具体协议,通常设为 0,让系统根据 domaintype 自动选择默认协议


2.2.2 绑定端口号和IP地址

如果把 socket() 比作买了一部电话机,那么这段代码就是在去电信局申请电话号码(端口)并把电话线插到墙上的电话口(网卡)上,准备开始接听电话。

cpp 复制代码
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()); // 转换为网络字节序
local.sin_addr.s_addr = htonl(INADDR_ANY); // 绑定本机所有IP地址

int n = bind(sockfd_, (struct sockaddr*)&local, sizeof(local));
if(n < 0)
{
    logger(Fatal, "socket bind error, errno: %d, err string: %s", errno, strerror(errno));
    exit(BIND_ERROR);
}
logger(Info, "socket bind success, port: %d, ip: %s", port_, ip_.c_str());

struct sockaddr_in 结构体用于存储需要绑定的信息,告诉套接字应该监听本机的哪个 IP 和哪个端口

转到定义就发现这个宏其实就是 sin_family##C 语言里的一种拼接用法

sin_port 本质上是 16 比特位类型

sin_addr 本质是 32 比特位类型

首先对创建的结构体 local,使用 bzero 对结构体初始化清零,然后依次设置 sin_familysin_portsin_addr,注意端口和 IP 都需要转为网络序列,因为网络设备无法确定他的大小端优先级,最后进行 bind 绑定

  • h = host (主机,即你的电脑)
  • n = network (网络)
  • to = 转换方向
  • s = short (16 位整数,通常用于端口号)
  • l = long (32 位整数,通常用于 IPv4 地址)
功能 老式函数 (仅IPv4) 现代推荐函数 (支持IPv4 & IPv6) 优势
IP字符串 转 二进制 inet_addr / inet_aton inet_pton 支持 IPv6,返回值处理更清晰
二进制 转 IP字符串 inet_ntoa inet_ntop 线程安全(需要用户自己提供 buffer),支持 IPv6
  • p = presentation (表达格式,即字符串)
  • n = numeric (数值格式,即二进制)
  • pton = p to n (字符串转数字)
  • ntop = n to p (数字转字符串)

2.2.3 启动服务端收发功能

cpp 复制代码
isrunning_ = true;
char inbuffer[size];
while(true)
{
    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)
    {
        logger(Warning, "recvfrom client data error, n: %ld, errno: %d, err string: %s", n, errno, strerror(errno));
        continue;
    }
    inbuffer[n] = 0;
    std::string info = inbuffer;
    std::string echo_string_c = "client echo# " + info + " [这是客户端发送的信息]";
    std::cout << echo_string_c << std::endl;
    std::string echo_string_s = "server echo# " + info + " [这是服务器端发送的信息]";
    sendto(sockfd_, echo_string_s.c_str(), echo_string_s.size(), 0, (struct sockaddr*)&client, len);
    std::cout << echo_string_s << std::endl;
  1. sockfd:套接字描述符(Socket)。
  2. buf:要发送的数据缓冲区指针(信的内容)。
  3. len:要发送的数据长度(信的字数)。
  4. flags :通常设为 0
  5. dest_addr(关键) 目标地址结构体指针。包含接收方的 IP 和端口。
  6. addrlen :目标地址结构体的长度(sizeof(struct sockaddr_in))。
  1. sockfd:套接字描述符。
  2. buf:接收数据的缓冲区(准备好的空信纸)。
  3. len:缓冲区的大小(防止溢出)。
  4. flags :通常设为 0
  5. src_addr(关键,输出参数) 这是一个空指针,函数返回时,系统会把发送方 的 IP 和端口填在这个结构体里。如果你不关心谁发的,可以填 NULL
  6. addrlen(关键,输入输出参数)

3.客户端

cpp 复制代码
#include <iostream>
#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <cstring>
#include <string>

using namespace std;

void Usage(const char *proc)
{
    std::cout << "\n\rUsage: " << proc << " serverip serverport\n" << std::endl;
}

int main(int argc, char *argv[])
{
    if(argc != 3)
    {
        Usage(argv[0]);
        exit(0);
    }
    string serverip = argv[1];
    uint16_t serverport = stoi(argv[2]);

    // 1.创建套接字
    int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    if(sockfd < 0)
    {
        std::cerr << "socket create error, sockfd: " << sockfd << std::endl;
        exit(1);
    }
    std::cout << "socket create success, sockfd: " << sockfd << std::endl;
    // 2.准备服务器地址信息
    struct sockaddr_in serveraddr;
    bzero(&serveraddr, sizeof(serveraddr));
    serveraddr.sin_family = AF_INET;
    serveraddr.sin_port = htons(serverport); // 转换为网络字节序
    serveraddr.sin_addr.s_addr = inet_addr(serverip.c_str()); // 转换为网络字节序   
    // 3.发送数据
    char buffer[1024];
    while(true)
    {
        cout << "Please Enter@ ";
        cin.getline(buffer, sizeof(buffer));
        sendto(sockfd, buffer, strlen(buffer), 0, (struct sockaddr*)&serveraddr, sizeof(serveraddr));

        // 接收数据
        struct sockaddr_in tmp;
        socklen_t len = sizeof(tmp);
        ssize_t n = recvfrom(sockfd, buffer, sizeof(buffer)-1, 0, (struct sockaddr*)&tmp, &len);
        if(n > 0)
        {
            buffer[n] = 0;
            cout << buffer << endl;
        }
    }
    // 4.关闭套接字
    close(sockfd);
    return 0;
}

客户端的代码和服务端大致相同,不同地方在于客户端需要指定的是自己要链接哪个端口和 IP 地址,该IP地址是公网IP,端口是指定服务端的端口

4.运行效果

5.常用网络命令

5.1 ping


ping 命令是用于测试两台设备之间网络连通性、检测延迟和丢包率的网络诊断工具

5.2 netstat

netstat 是一款用于查看本机网络连接状态、路由表、网络接口信息的命令行工具,可帮助排查网络连接问题、监控端口占用情况

参数 作用 备注
-a 显示所有网络连接(包括监听、已建立、关闭等待等状态) 最基础的全量查看参数
-n 数字形式显示 IP 和端口(不解析域名、不转换端口名称) 提升执行速度,避免 DNS 解析延迟
-p 显示进程信息(进程 ID + 进程名称) 需要 root 权限sudo)才能查看所有用户的进程
-t 仅筛选 TCP 协议的连接 聚焦 TCP 相关的监听/连接状态
-u 仅筛选 UDP 协议的连接 UDP 无连接状态,仅显示监听端口
-l 仅显示监听状态的端口 排查服务是否正常监听(如 Web 服务 80 端口)
-r 显示内核路由表 等同于 route -n 命令
-i 显示网络接口的流量统计(收发字节数、丢包数等) 排查网卡是否有异常丢包

希望读者们多多三连支持

小编会继续更新

你们的鼓励就是我前进的动力!

相关推荐
阿拉伯柠檬2 小时前
应用层协议HTTP
linux·网络·c++·网络协议·http
Ha_To2 小时前
2025.12.24 Cisco防火墙ASA与动态PAT配置
linux·服务器·网络
是娇娇公主~2 小时前
TCP拥塞控制
网络协议·tcp/ip·php
蒟蒻的贤2 小时前
计算机网络简答题
计算机网络
就不掉头发2 小时前
UDP编程
网络·网络协议·udp
Lily.C2 小时前
小程序WebSocket实时通信全解析
websocket·网络协议·小程序
代码游侠3 小时前
学习笔记——TCP 传输控制协议
linux·网络·笔记·网络协议·学习·tcp/ip
ICT技术最前线3 小时前
华为交换机VLAN配置命令详解
服务器·网络·vlan·华为交换机
zhendianluli3 小时前
为什么fclose处理的是file而不是fd
linux·服务器·网络