网络通信:套接字编程全解析

目录

一.网络通信

1.创建套接字

[1. domain(协议域/地址族)](#1. domain(协议域/地址族))

[2. type(套接字类型)](#2. type(套接字类型))

[3. protocol(协议)](#3. protocol(协议))

2.绑定套接字

返回值

[参数1: sockfd - 套接字描述符](#参数1: sockfd - 套接字描述符)

[参数2: addr - 地址结构体指针](#参数2: addr - 地址结构体指针)

[1. sin_family(地址族)](#1. sin_family(地址族))

[2. sin_port(端口号)](#2. sin_port(端口号))

[3. sin_addr(IP地址)](#3. sin_addr(IP地址))

[4. sin_zero(填充)可省略](#4. sin_zero(填充)可省略)

bzero:

[参数3: addrlen - 地址结构体长度](#参数3: addrlen - 地址结构体长度)

3.接收信息recvfrom()

[src_addr - 发送方地址(重要!)](#src_addr - 发送方地址(重要!))

[addrlen - 地址结构体大小](#addrlen - 地址结构体大小)

4.发送信息sendto()

[dest_addr - 目标地址(关键参数)](#dest_addr - 目标地址(关键参数))

[addrlen - 地址结构体大小](#addrlen - 地址结构体大小)

5.netstat


一.网络通信

1.创建套接字

套接字 是计算机网络编程中的核心概念,可以理解为网络通信的端点网络上的"插座"

通俗理解:

想象一下生活中的插座:

  • 物理插座:电器插入插座就能获得电力

  • 网络套接字:程序"插入"套接字就能通过网络收发数据

更准确的类比:

  • 套接字就像电话机

    • 创建套接字 = 安装一部电话

    • bind(绑定端口) = 分配一个电话号码

    • connect/accept = 拨打电话/接听电话

    • send/recv = 通话交流

    • close = 挂断电话

cpp 复制代码
#include <sys/socket.h>

int socket(int domain, int type, int protocol);
  • 成功:返回一个非负整数(套接字文件描述符)

  • 失败 :返回 -1,并设置 errno

1. domain(协议域/地址族)

指定通信的协议族,决定了地址格式:

常量 说明 地址格式示例
AF_INET IPv4 互联网协议族 192.168.1.1:8080
AF_INET6 IPv6 互联网协议族 [2001:db8::1]:8080
AF_UNIX / AF_LOCAL Unix 域协议(本地通信) 文件路径 /tmp/socket
AF_PACKET 底层数据包接口(需root权限) 网卡级别
AF_BLUETOOTH 蓝牙协议 蓝牙地址

2. type(套接字类型)

指定通信语义:

常量 说明 特点
SOCK_STREAM 流式套接字(TCP) 可靠、有序、面向连接、字节流
SOCK_DGRAM 数据报套接字(UDP) 不可靠、无连接、数据报
SOCK_RAW 原始套接字 直接访问IP层(需root)
SOCK_SEQPACKET 顺序数据包套接字 可靠、有序、面向连接、数据报边界
SOCK_RDM 可靠数据报 可靠但可能乱序(很少用)

3. protocol(协议)

通常设为 0,让系统自动选择协议:

说明 适用场景
0 自动选择 99%的情况都用这个
IPPROTO_TCP 明确指定TCP type=SOCK_STREAM
IPPROTO_UDP 明确指定UDP type=SOCK_DGRAM
IPPROTO_ICMP ICMP协议 原始套接字实现ping
IPPROTO_RAW 原始IP数据包 自定义IP协议

对于我们写Udp网络传输来说,我么你创建套接字的参数是固定的。

2.绑定套接字

cpp 复制代码
#include <sys/socket.h>

int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

返回值

  • 成功:返回 0

  • 失败:返回 -1,设置 errno

角色 是否需要 bind 原因
服务器端 必须 需要固定的地址和端口,让客户端知道如何连接
客户端 可选 系统会自动分配临时端口(通常不需要手动 bind)

参数1: sockfd - 套接字描述符

  • socket() 函数返回的文件描述符

  • 是一个非负整数,代表已创建的套接字

  • 类似于文件操作的 fd,但用于网络通信

错误 原因 解决
EBADF sockfd 无效(如已关闭) 检查 socket 是否成功创建
ENOTSOCK sockfd 不是 socket 检查是否传错了文件描述符

参数2: addr - 地址结构体指针

cpp 复制代码
struct sockaddr {
    sa_family_t sa_family;  // 地址族,如 AF_INET
    char sa_data[14];       // 地址数据(IP+端口)
};

注意:实际编程中不使用这个结构,只用于类型转换。通常使用IPV4来转化

cpp 复制代码
struct sockaddr_in {
    sa_family_t    sin_family;   // 地址族,固定为 AF_INET
    in_port_t      sin_port;     // 端口号(16位)
    struct in_addr sin_addr;     // IPv4 地址(32位)
    char           sin_zero[8];  // 填充字节,使结构大小相同
};

struct in_addr {
    in_addr_t s_addr;  // 32位 IPv4 地址,网络字节序
};
1. sin_family(地址族)
cpp 复制代码
struct sockaddr_in addr;
addr.sin_family = AF_INET;  // IPv4
// 其他可能值:
// AF_INET6 - IPv6
// AF_UNIX  - Unix域
// AF_PACKET - 数据链路层
2. sin_port(端口号)

范围

  • 0-1023:系统保留端口(需要 root 权限)

  • 1024-49151:注册端口(普通用户可用)

  • 49152-65535:动态/私有端口

cpp 复制代码
// 设置端口(必须转换到网络字节序)
addr.sin_port = htons(8080);   // HTTP 备用端口
addr.sin_port = htons(80);     // HTTP 标准端口(需要 root)
addr.sin_port = htons(443);    // HTTPS(需要 root)
addr.sin_port = htons(53);     // DNS(需要 root)
addr.sin_port = htons(3306);   // MySQL

// 特殊用法:端口为 0,系统自动分配
addr.sin_port = htons(0);
3. sin_addr(IP地址)
cpp 复制代码
// 方式1:绑定到所有网卡(最常用)
addr.sin_addr.s_addr = INADDR_ANY;  // 等同于 0.0.0.0

// 方式2:绑定到本地回环(仅本机访问)
addr.sin_addr.s_addr = inet_addr("127.0.0.1");
// 或使用 inet_pton(推荐,支持 IPv6)
inet_pton(AF_INET, "127.0.0.1", &addr.sin_addr);

// 方式3:绑定到特定 IP
inet_pton(AF_INET, "192.168.1.100", &addr.sin_addr);

// 方式4:绑定到广播地址
addr.sin_addr.s_addr = INADDR_BROADCAST;  // 255.255.255.255

// 方式5:使用宏
addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK);  // 127.0.0.1
addr.sin_addr.s_addr = htonl(INADDR_ANY);       // 0.0.0.0
cpp 复制代码
#include <arpa/inet.h>

int inet_pton(int af, const char *src, void *dst);





#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

int inet_aton(const char *cp, struct in_addr *inp);




#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

in_addr_t inet_addr(const char *cp);
函数 协议支持 线程安全 可重入 推荐程度
inet_pton IPv4/IPv6 ✅ 安全 ✅ 是 ⭐⭐⭐⭐⭐ 最推荐
inet_aton IPv4 only ✅ 安全 ✅ 是 ⭐⭐⭐ 仅 IPv4
inet_addr IPv4 only ❌ 返回 INADDR_NONE 有歧义 ✅ 是 ⭐ 已废弃
函数 协议支持 线程安全 可重入 推荐程度
inet_ntop IPv4/IPv6 ✅ 安全 ✅ 是 ⭐⭐⭐⭐⭐ 最推荐
inet_ntoa IPv4 only ❌ 使用静态缓冲区 ❌ 否 ⭐ 已废弃
cpp 复制代码
#include <arpa/inet.h>

const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);

​​​​​​​参数说明:

  • af:地址族

    • AF_INET:IPv4

    • AF_INET6:IPv6

  • src:指向二进制地址的指针

    • IPv4:struct in_addr*

    • IPv6:struct in6_addr*

  • dst:输出缓冲区,用于存储转换后的字符串

  • size:输出缓冲区的大小

4. sin_zero(填充)可省略
  • 只是为了确保 sockaddr_insockaddr 大小相同

  • 通常用 memset() 清零

  • 现代编程中可以忽略,但建议填充

注意:一般来说使用时会手动清0

这时我们介绍一个清零结构体的函数方法:

bzero:

bzero 是一个用于将内存区域清零的函数,名字来源于 "byte zero"(字节置零)。

cpp 复制代码
#include <strings.h>  // 注意:不是 string.h,是 strings.h

void bzero(void *s, size_t n);
参数 说明
s 指向要清零的内存区域的指针
n 要清零的字节数
cpp 复制代码
 bzero(&local,sizeof(local));

参数3: addrlen - 地址结构体长度

告诉内核地址结构体的实际大小,防止缓冲区溢出。

不同结构体的大小

cpp 复制代码
// 计算大小的方式
size_t len1 = sizeof(struct sockaddr_in);   // 16 字节

size_t len4 = sizeof(struct sockaddr);      // 16 字节

// bind 使用
bind(sockfd, (struct sockaddr*)&addr, sizeof(addr));

常见错误

cpp 复制代码
// 错误1:长度不足
bind(sockfd, (struct sockaddr*)&addr, 10);  // 太小!

// 错误2:长度过大(虽然不报错,但没必要)
bind(sockfd, (struct sockaddr*)&addr, 1024);

// 错误3:使用错误的类型大小
struct sockaddr_in6 addr6;
bind(sockfd, (struct sockaddr*)&addr6, sizeof(struct sockaddr_in));  // 错误!大小不匹配

3.接收信息recvfrom()

recvfrom() 是 UDP 套接字编程中最核心的函数之一,用于接收数据,并且能够同时获取发送方的地址信息

cpp 复制代码
#include <sys/types.h>
#include <sys/socket.h>

ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
                 struct sockaddr *src_addr, socklen_t *addrlen);
参数 类型 说明
sockfd int 套接字描述符(由 socket() 返回)
buf void* 接收数据的缓冲区指针
len size_t 缓冲区大小(最多接收多少字节)
flags int 接收选项(通常设为 0)
src_addr struct sockaddr* 输出参数,存储发送方的地址信息
addrlen socklen_t* 输入输出参数,地址结构体的大小
  • 成功:返回实际接收的字节数(0 表示接收到空数据报)

  • 失败:返回 -1,并设置 errno

src_addr - 发送方地址(重要!)

这是 UDP 的关键特性:UDP 是无连接的,每次接收数据时都能知道是谁发来的。

cpp 复制代码
struct sockaddr_in client_addr;
socklen_t addrlen = sizeof(client_addr);

recvfrom(sockfd, buffer, 1024, 0, 
         (struct sockaddr*)&client_addr, &addrlen);

// 现在可以知道是谁发的数据
char ip[INET_ADDRSTRLEN];
inet_ntop(AF_INET, &client_addr.sin_addr, ip, sizeof(ip));
int port = ntohs(client_addr.sin_port);
printf("Received from %s:%d\n", ip, port);

注意 :如果不关心发送方地址,可以设为 NULL

cpp 复制代码
recvfrom(sockfd, buffer, 1024, 0, NULL, NULL);

addrlen - 地址结构体大小

这是一个输入输出参数

cpp 复制代码
struct sockaddr_in client_addr;
socklen_t addrlen = sizeof(client_addr);  // 输入:告诉内核结构体大小

recvfrom(sockfd, buffer, 1024, 0, 
         (struct sockaddr*)&client_addr, &addrlen);

// 调用后,addrlen 会被设置为实际写入的地址长度
// 通常还是 sizeof(struct sockaddr_in)

4.发送信息sendto()

sendto() 是 UDP 套接字编程中用于发送数据 的核心函数,与 recvfrom() 相对应。它允许你向指定的目标地址发送数据报。

cpp 复制代码
#include <sys/types.h>
#include <sys/socket.h>

ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
               const struct sockaddr *dest_addr, socklen_t addrlen);
参数 类型 说明
sockfd int 套接字描述符(由 socket() 返回)
buf const void* 要发送的数据缓冲区指针
len size_t 要发送的数据长度(字节数)
flags int 发送选项(通常设为 0)
dest_addr struct sockaddr* 目标地址信息(接收方的 IP 和端口)
addrlen socklen_t dest_addr 结构体的大小
  • 成功 :返回实际发送的字节数(通常等于 len

  • 失败:返回 -1,并设置 errno

dest_addr - 目标地址(关键参数)

这是 UDP 的核心:每次发送都要指定目标地址,因为 UDP 是无连接的。

cpp 复制代码
struct sockaddr_in target_addr;
memset(&target_addr, 0, sizeof(target_addr));
target_addr.sin_family = AF_INET;
target_addr.sin_port = htons(8080);           // 目标端口
inet_pton(AF_INET, "192.168.1.100", &target_addr.sin_addr);  // 目标 IP

sendto(sockfd, buffer, len, 0, 
       (struct sockaddr*)&target_addr, sizeof(target_addr));

addrlen - 地址结构体大小

cpp 复制代码
// 必须是 dest_addr 指向的结构体的实际大小
sendto(sockfd, buffer, len, 0, 
       (struct sockaddr*)&target_addr, sizeof(target_addr));
cpp 复制代码
#pragma once

#include <iostream>
#include <string>
#include <memory>
#include <cstring>
#include <cerrno>
#include <strings.h>

#include <sys/types.h>
#include <sys/socket.h> // 提供 socket()、bind() 等函数
#include <netinet/in.h> // 提供 sockaddr_in、in_addr 等
#include <arpa/inet.h>  // 提供 inet_pton()、inet_ntop() 等转换函数(可选)

#include "Log.hpp"
#include "Mutex.hpp"

using namespace LogMudule;
#define Die(code)   \
    do              \
    {               \
        exit(code); \
    } while (0)
#define CONV(v) (struct sockaddr *)(v) // 将socketaddr_in转化为sockaddr

const static int gsocket = -1;
const static uint16_t gport = 8080;
const static std::string gip = "127.0.0.1";
class UdpSever
{
public:
    UdpSever(uint16_t port = gport, std::string ip = gip)
        : _socket(gsocket),
          _ip(ip),
          _port(port),
          _isruing(false)
    {
    }
    void InintSever()
    {
        // 1.创建套接字
        // 创建一个套接字第一个参数是选取网络协议流,
        // 第二个参数是采用Udp数据流传输,
        // 第三个是协议通常为0系统自己选择
        _socket = ::socket(AF_INET, SOCK_DGRAM, 0);
        // 创建失败使用日志报错并直接退出结束进程
        if (_socket < 0)
        {
            Log(LogLeval::FATAL) << "socket:" << strerror(errno);
            Die(1);
        }
        Log(LogLeval::INFO) << "socket success ,sockfd is:" << _socket;
        // 2.绑定套接字分配一个端口号相当于一个分配一个电话号码
        struct sockaddr_in local;
        // 清零结构体
        bzero(&local, sizeof(local));
        local.sin_family = AF_INET;
        local.sin_addr.s_addr = ::inet_addr(_ip.c_str());
        local.sin_port = htons(_port); // 传到网络中时需要区分大小端htons()统一将端口改为大端
        // local.sin_zero=;

        int n = ::bind(_socket, CONV(&local), sizeof(local));
        if (n < 0)
        {
            Log(LogLeval::FATAL) << "bind:" << strerror(errno);
            Die(2);
        }
        Log(LogLeval::INFO) << "bind success";
    }
    void Start()
    {
        _isruing = true;
        while (true)
        {
            sockaddr_in local;
            char buf[1024];
            socklen_t len = sizeof(local);
            int n = recvfrom(_socket, buf, strlen(buf), 0, CONV(&local), &len);
            if (n > 0)
            {
                buf[n] = 0;
                Log(LogLeval::INFO) << "client say:" << buf << strerror(errno);

                std::string sbuf="echo#";
                sbuf+=buf;

                int n=sendto(_socket,buf,strlen(buf),0,CONV(&local),len);
            }
        }
    }
    uint16_t GetPort()
    {
        return _port;
    }
    ~UdpSever()
    {
    }

private:
    int _socket;
    uint16_t _port;  // 端口号
    std::string _ip; // ip
    bool _isruing;   // 服务器是否正常运行
};

这时我们一个简单的服务端就做好了,不过我们现在没有客户端,只能通过sheel来查看代码的正确性

5.netstat

netstat (network statistics) 是一个用于显示网络连接、路由表、接口状态等信息的命令行工具

netstat 通过组合不同的参数来筛选和展示信息。

类别 选项 说明
显示类型 -a (all) 显示所有连接和监听端口。
-l (listening) 显示正在监听的端口(服务端常用)。
协议筛选 -t (tcp) 仅显示TCP协议的连接。
-u (udp) 仅显示UDP协议的连接。
信息展示 -n (numeric) 数字形式显示IP和端口,避免域名解析,执行更快。
-p (program) 显示对应连接的进程ID(PID)和程序名(需root权限获取完整信息)。
-e (extend) 显示更多扩展信息。
其他功能 -r (route) 显示路由表 ,功能类似route命令。
-i (interfaces) 显示网络接口的流量统计(如收发包数、错误数等)。
-s (statistics) 按协议(IP、TCP、UDP等)显示详细的统计信息
-c (continuous) 每隔一秒持续输出,用于实时监控。
相关推荐
取经蜗牛1 小时前
Ubuntu 国内镜像源配置指南(多版本常用镜像地址都有)
linux·运维·ubuntu
实心儿儿3 小时前
Linux —— 线程控制(1)
linux·运维·服务器
筠筠喵呜喵3 小时前
Linux软件开发性能优化
linux·c++·性能优化
仰泳之鹅3 小时前
【物联网】使用MQTTX与OneNET云平台进行模拟MQTT协议通信
网络·物联网
Bruce_kaizy3 小时前
c++ linux环境编程——文件io介绍以及open 、write 、read 三剑客深度详解
linux·服务器·c++·ubuntu·操作系统·文件io
亦良Cool4 小时前
VMware虚拟机ubuntu瘦身,解决虚拟机越用越大
linux·运维·ubuntu
星辰&与海5 小时前
KVM + QEMU虚拟化方案
linux·运维
宋浮檀s5 小时前
应急响应——恶意流量&攻击行为识别
linux·运维·网络·网络安全·应急响应
REDcker5 小时前
Linux OverlayFS详解
java·linux·运维
yychen_java5 小时前
6G移动通信:当网络开始“思考”与“感知”
网络·人工智能