目录
[1. domain(协议域/地址族)](#1. domain(协议域/地址族))
[2. type(套接字类型)](#2. type(套接字类型))
[3. protocol(协议)](#3. protocol(协议))
[参数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(填充)可省略)
[参数3: addrlen - 地址结构体长度](#参数3: addrlen - 地址结构体长度)
[src_addr - 发送方地址(重要!)](#src_addr - 发送方地址(重要!))
[addrlen - 地址结构体大小](#addrlen - 地址结构体大小)
[dest_addr - 目标地址(关键参数)](#dest_addr - 目标地址(关键参数))
[addrlen - 地址结构体大小](#addrlen - 地址结构体大小)
一.网络通信
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_in和sockaddr大小相同 -
通常用
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) |
每隔一秒持续输出,用于实时监控。 |
