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

目录

一.网络通信

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) 每隔一秒持续输出,用于实时监控。
相关推荐
用户97183563346644 分钟前
银河麒麟 KY10 申威(SW64) 安装 nginx-1.16.1-2.p01.ky10.sw_64.rpm 详细步骤
linux
猪脚踏浪2 小时前
linux 拷贝文件或目录到指定的位置
linux
摇滚侠18 小时前
Linux CentOS7 rpm 安装 MySQL 5.7
linux·运维·mysql
bush418 小时前
嵌入式linux学习记录十四、术语
linux·嵌入式
载数而行52019 小时前
Linux 11 动态监控指令top
linux
网络研究院20 小时前
2026年网络安全
网络·安全·法律·法规·趋势·发展
酣大智20 小时前
ARP代理--工作原理
运维·网络·arp·arp代理
treesforest20 小时前
AI安全系统如何识别异常访问?IP风险识别正在成为关键能力
网络·人工智能·tcp/ip·安全·web安全
不会C语言的男孩20 小时前
Linux 系统编程 · 第 8 章:进程基础
linux·c语言
shushangyun_20 小时前
2026年快消品B2B系统推荐:支持终端门店订货、促销政策自动化的工具?
java·运维·网络·数据库·人工智能·spring·自动化