socket编程 【C语言】

socket编程

一、核心头文件

linux系统下的C语言socket头文件:

c 复制代码
#include <sys/socket.h>  // 定义 socket 核心函数与 SOCK_STREAM/SOCK_DGRAM 等宏
#include <netinet/in.h>  // 定义 sockaddr_in 结构体
#include <arpa/inet.h>   // 定义 htons()、inet_pton()、inet_addr() 等网络字节序转换函数
#include <unistd.h>      // 定义 close() 等 POSIX 系统调用

二、创建与销毁

1️⃣ 创建套接字

c 复制代码
int socket(int domain, int type, int protocol);
  • domain:网络层协议族。 AF_INET(IPv4)或 AF_INET6(IPv6)。

  • type(套接字类型):

    • SOCK_DGRAM:UDP --> 面向数据报
    • SOCK_STREAM:TCP --> 面向字节流
  • protocol:根据不同的domain和type组合来选择适合的协议。

    例如:

    Domain Type Protocol
    AF_INET SOCK_STREAM IPPROTO_TCP (TCP协议)
    AF_INET SOCK_DGRAM IPPROTO_UDP (UDP协议)
    通常填0 当参数传 0 时,操作系统内核根据前两个参数的组合,自动选择对应的协议

返回值 :成功返回非负;失败返回 -1 并设置 errno。

2️⃣关闭套接字

c 复制代码
int close(int fd);

fd:套接字描述符。

返回值 :成功返回 0;失败返回 -1。

三、地址绑定与连接控制

1️⃣绑定本地地址与端口

主要用于服务端,将套接字与固定的IP和端口绑定。

ℹ️ 服务端(如 Web 服务器的80端口、采集卡的20108端口)都是被动等待访问的。

调用bind把自己固定在20108端口上,否则系统会随机分配一个端口(今天1234,明天5678),客户端不能找到它。
ℹ️ 客户端作为主动方通常省去bind。

首次调用 connect() 或 sendto() 时,操作系统内核会自动为其分配临时随机端口。

进阶说明:客户端并非不能绑定。若板卡写死了上位机的回包端口,客户端需在发生前bind锁定本地端口,确保能正常接收数据。

c 复制代码
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
  • sockfd:待绑定的本地套接字描述符

  • addr:指向通用地址结构体 struct sockaddr 的指针

    实际使用时传入 struct sockaddr_in(IPv4),再强转(struct sockaddr *)。

    举例:

    c 复制代码
    struct sockaddr_in addr;
    bind(sockfd, (struct sockaddr*)&addr, sizeof(addr));
  • addrlen:结构体内存大小

返回值 :成功返回 0;失败返回 -1(如端口被占用)。

2️⃣建立连接

专用于 TCP 客户端 ,向服务端发起三次握手连接:

bash 复制代码
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
  • sockfd: 本地创建的 TCP 套接字描述符

  • addr: 服务端的地址结构体

    实际使用时传入 struct sockaddr_in(IPv4),再强转(struct sockaddr *)。

返回值 : 成功返回 0;失败返回 -1。

3️⃣配置套接字属性

🎯在绑定或收发前微调套接字的模式(设置超时、端口复用)。

c 复制代码
int setsockopt(int sockfd, int level, int optname, const void *optval, socklen_t optlen);
  • sockfd:本地创建的套接字描述符。
  • level:协议层级。常选 SOL_SOCKET(套接字通用级别)。
  • optname:选项名。
    • SO_REUSEADDR:允许地址/端口快速复用。
    • SO_RCVTIMEO:设置数据接收超时时间(传入 struct timeval)。
  • optval:指向存储选项值的缓冲区指针(如 struct timeval tv;)。
  • optlen:参数结构体的字节大小。

返回值 :成功返回 0;失败返回 -1。

举例:

c 复制代码
// 设置接收超时时间500ms,避免永久阻塞
#include <sys/time.h>
struct timeval tv;
tv.tv_sec = 0;
tv.tv_usec = 500000; // 500 ms
ret = setsockopt(sockfd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));

四、UDP通信接口(面向无连接)

1.UDP特性

  • 无连接、无状态:没有 TCP 的三次握手与状态维护,直接通过 sendto 指定目标 IP:Port 发送

  • 保留消息边界:应用层一次 sendto 产生一个数据报,接收方一次 recvfrom 接收一个数据报,不存在 TCP 的粘包问题

  • 端口绑定:

    • 接收端(服务端):必须 bind() 固定端口,否则外界不知道通信的目的地址。

    • 发送端(客户端):通常不 bind()。第一次调用 sendto 时,Linux 内核随机分配一个临时空闲端口。

UDP 像"发快递",每次收发独立,不需要维持长连接。

2.核心函数

1️⃣发送数据报

特点:由于没有连接状态(无需connect),每次发送必须携带目标主机的IP和端口。

c 复制代码
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
               const struct sockaddr *dest_addr, socklen_t addrlen);
  • sockfd:本地套接字描述符。
  • buf:存放待发送数据内存的起始地址。
  • len:待发送数据的字节数。
  • flags:调用标志位,通常填 0。
  • dest_addr:目标主机的地址结构体指针(包含目标 IP 和端口)。
  • addrlen:目标地址结构体的大小 sizeof(...)。

返回值 :成功返回实际发送出的字节数;失败返回 -1。

举例:

c 复制代码
int sendLen = sendto(sockfd, buf, len, 0, (struct sockaddr*)destAddr, sizeof(struct sockaddr_in));

2️⃣ 接收数据报

特点:接收数据报,同时可以通过 src_addr 获取数据包来自哪个发送端。

c 复制代码
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
                 struct sockaddr *src_addr, socklen_t *addrlen);
  • sockfd:本地套接字描述符

  • buf:存放接收数据的缓冲区指针。

  • len:缓冲区容纳字节数

  • flags:调用标志位,通常填 0。

  • src_addr:输出型参数,内核把发送方的 IP 和端口 写进这个结构体。若不需回信,可传 NULL。

  • addrlen:输入输出型参数,传入 src_addr 缓冲区的初始大小,接收后内核写回实际写入的地址长度。

返回值 :成功返回实际读取到的字节数;若设定了超时且未收到包,返回 -1 且 errno 变为 EAGAIN;发生系统故障返回 -1。

举例:

c 复制代码
recvfrom(sockfd, buf, sizeof(buf), 0, (struct sockaddr*)src_addr, &addrlen);

3.UDP核心通信流程

markdown 复制代码
       【发送端 (Client)】                          【接收端 (Server)】
               │                                            │
        1. socket() 创建套接字                       1. socket() 创建套接字
               │                                            │
               │                              2. bind() 绑定本地监听端口 (如: 8080)
               │                                            │
               │                                  3. setsockopt() 设置超时 (防死锁)
               │                                            │
        2. sendto() 填入目标 IP:Port ──► [物理网线] ──► 4. recvfrom() 阻塞等待数据
               │                                            │
               │                                     (提取数据并获取发送方 IP:Port)
               │                                            │
        3. close() 关闭套接字                          5. close() 关闭套接字

4.UDP 客户端(发送模板)

c 复制代码
#include <arpa/inet.h>
#include <string.h>
#include <unistd.h>

int main() {
    // 1. 创建 UDP 套接字
    int sock = socket(AF_INET, SOCK_DGRAM, 0);

    // 2. 配置目标地址 (IP 与 端口转网络字节序)
    struct sockaddr_in dest_addr;
    memset(&dest_addr, 0, sizeof(dest_addr));
    dest_addr.sin_family = AF_INET;
    dest_addr.sin_port = htons(8888);
    inet_pton(AF_INET, "192.168.1.20", &dest_addr.sin_addr);

	// 3. 发送数据报 (单次投递,必须携带目标地址)
    unit8_t tx_buf[] = {0xAA, 0x55, 0x01};
    sendto(sock, tx_buf, sizeof(tx_buf), 0, 
           (struct sockaddr*)&dest_addr, sizeof(dest_addr));

    // 4. 关闭套接字
    close(sock);
    return 0;
}

5.UDP 服务端(接收模板)

c 复制代码
#include <arpa/inet.h>
#include <string.h>
#include <unistd.h>

int main() {
    // 1.创建 UDP 套接字
    int sock = socket(AF_INET, SOCK_DGRAM, 0);

    // 2.配置本地监听端口
    struct sockaddr_in local_addr;
    memset(&local_addr, 0, sizeof(local_addr));
    local_addr.sin_family = AF_INET;          // IPv4
    local_addr.sin_port = htons(8888);        // 端口号
    local_addr.sin_addr.s_addr = INADDR_ANY;  // IP地址,INADDR_ANY监听任意IP发来的数据
    // 3.绑定
    bind(sock, (struct sockaddr*)&local_addr, sizeof(local_addr));

    // 4.阻塞接收数据报并记录发送端信息
    char rx_buf[1024]; // 定义数组,存放1024个char类型的元素
    struct sockaddr_in client_addr;
    socklen_t client_len = sizeof(client_addr);
    ssize_t len = recvfrom(sock, rx_buf, sizeof(rx_buf) - 1, 0,
                           (struct sockaddr*)&client_addr, &client_len);
    // 纯接收,不关心是谁发的,也不回信
	// ssize_t len = recvfrom(sock, rx_buf, sizeof(rx_buf) - 1, 0, NULL, NULL);

    // 5.关闭套接字
    close(sock);
    return 0;
}

INADDR_ANY 本质是 0.0.0.0,代表绑定本机所有网卡 。工控机可能有多个网口(如 eth0、eth1、回环 127.0.0.1),写 INADDR_ANY 意味着无论外部数据从哪个网口进来,只要端口是 8888,程序都能收到。如果写死某个 IP,就只能监听那个网口。

五、TCP通信接口(面向连接)

TCP 像"打电话",通信前必须先建链(三次握手),之后通过稳定的双向字节流进行收发。

1. TCP 客户端(发送模板)

c 复制代码
// 1. 创建 TCP 套接字
int sock = socket(AF_INET, SOCK_STREAM, 0);

// 2. 配置目标服务器地址
struct sockaddr_in server_addr;
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(20108); // 端口
inet_pton(AF_INET, "192.168.1.7", &server_addr.sin_addr); // IP

// 3. 主动发起 connect 连接
connect(sock, (struct sockaddr*)&server_addr, sizeof(server_addr));

// 4. 数据收发 (无需目标地址,直接读写管道)
send(sock, tx_buf, tx_len, 0);
int recv_len = recv(sock, rx_buf, sizeof(rx_buf), 0);

// 5. 关闭连接
close(sock);

2. TCP 服务端(接收模板)

c 复制代码
// 1. 创建监听套接字
int server_sock = socket(AF_INET, SOCK_STREAM, 0);

// 2. 绑定本地端口
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(8888);
addr.sin_addr.s_addr = INADDR_ANY;
bind(server_sock, (struct sockaddr*)&addr, sizeof(addr));

// 3. 开启监听 (进入监听状态,参数 5 表示最大挂起连接数)
listen(server_sock, 5);

// 4. 阻塞等待客户端连接
struct sockaddr_in client_addr;
socklen_t client_len = sizeof(client_addr);
int client_sock = accept(server_sock, (struct sockaddr*)&client_addr, &client_len);

// 5. 与已连接的客户端收发数据
recv(client_sock, buf, sizeof(buf), 0);
send(client_sock, "OK", 2, 0);

// 6. 关闭套接字
close(client_sock);
close(server_sock);

六、地址与字节序转换

字节序

  • 每个地址存8bit

    • 无论计算机 32 / 64 位,每个内存地址只存 8 bit。

    • 32位 / 64位 CPU,指 CPU 寄存器位宽和数据总线能力(一次搬运或计算 4 / 8 个地址的数据)。

  • 单字节(char):独占单个地址,无排列先后,不存在大小端问题。

  • 多字节(int):跨多个连续地址,需规定高低位字节与内存地址的存放映射(大小端/字节序)。

大端序与小端序

以16 位十六进制整数 0x1234(2 字节)为例:

  • 0x12:高位字节
  • 0x34:低位字节
markdown 复制代码
大端序:
内存地址:    [ 地址 0 (低地址) ]      [ 地址 1 (高地址) ]
存储内容:          0x12                    0x34
【核心】符合人类从左到右的阅读习惯
markdown 复制代码
小端序:
内存地址:    [ 地址 0 (低地址) ]      [ 地址 1 (高地址) ]
存储内容:          0x34                    0x12
【核心】符合硬件算法进位逻辑

为什么工控机用小端,而网络用大端?

  • 大端序(网络序):TCP/IP 协议规范与 Modbus 工业协议统一要求。
  • 小端序(主机序):x86、ARM 绝大多数工控机 CPU 默认使用。

为什么通信需要转大端?

markdown 复制代码
┌─────────────────┬───────────────────┬───────────────────┬──────────────────────┐
│  以太网帧头      │     IP 报头        │     UDP 报头      │   应用层数据(Payload) │
│  (MAC 地址等)   │  (源 IP / 目的 IP) │ (源端口 / 目的端口)│    (Modbus 指令)     │
│    14 字节       │      20 字节       │       8 字节      │        8 字节        │
└─────────────────┴───────────────────┴───────────────────┴──────────────────────┘
                   ▲                   ▲                   ▲
                   │                   │                   └─ 对应 txBuf
                   │                   └─ 对应 htons(8234) / htons(20108)
                   └─ 对应 inet_pton 写入的 32 位大端 IP 地址 (4 字节)
规则划分:
  1. 网络协议头(IP 头、UDP 头):强制大端序 。网络设备统一按大端解析;小端主机若不转大端,IP/端口字节倒序(如 192.168.1.7 错乱为 7.1.168.192)会导致寻址失败。
  2. 应用层数据(Payload)**:**依协议约定。标准工业协议(如 Modbus-RTU)规范硬性要求大端;私有协议若两端架构一致且提前约定,亦可直接小端直传。

Linux C网络转换API

linux下的网络地址与字节序转换头文件:

c 复制代码
#include <arpa/inet.h>

1️⃣ 端口字节序转换 { 主机序(小端) ↔ 网络序(大端)}

c 复制代码
uint16_t htons(uint16_t hostshort); // Host to Network Short: 本地端口转网络序 (填 sin_port 必用)
//addr.sin_port = htons(8234); // 将主机端口 8234 转为大端序写入结构体

uint16_t ntohs(uint16_t netshort);  // Network to Host Short: 网络端口转本地主机序

2️⃣IP 地址转换(字符串↔\leftrightarrow↔ 二进制)

c 复制代码
int inet_pton(int af, const char *src, void *dst);
const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);
  • inet_pton(AF_INET, "192.168.1.7", &addr.sin_addr):解析 IP 字符串,以网络大端序(0xC0 0xA8 0x01 0x07)写入结构体,供 Linux 内核直接封装进 IP 报头作为目的 IP。
  • inet_ntop(AF_INET, &addr.sin_addr, ip_str, sizeof(ip_str)):从结构体反解出二级制为可读的 IP 字符串。

七、工程避坑

1️⃣ 结构体网络传输与字节对齐

隐患:不同架构(如 x86、ARM)或编译器默认存在 4/8 字节对齐补白,直接发送结构体会导致字段偏移错位。

规范:在定义网络传输协议帧时,必须强制 1字节紧凑对齐:

c 复制代码
#pragma pack(push, 1) // 开启 1 字节对齐
typedef struct {
    uint8_t  header;     // 帧头
    uint8_t  cmd;        // 命令字
    uint16_t data_len;   // 数据长度
    uint32_t timestamp;  // 时间戳
} NetPacket;
#pragma pack(pop)      // 恢复默认对齐
相关推荐
学逆向的2 小时前
C 局部静态变量
c语言·开发语言·网络安全
Zwawa10 小时前
从 printf 到曲线:把数据送上 VOFA+
c语言·单片机·嵌入式
zl_dfq11 小时前
Windows 下 UDP 套接字编程指南
网络·windows·udp
骑着蜗牛撵大象32713 小时前
手写一个 HTTP 服务器:从 socket 到 epoll,Linux 上到底怎么把请求变成响应
linux·服务器·网络·http·epoll·socket编程·并发模型
二十七日秋13 小时前
算法刷题笔记「Day 1|四道 OJ 入门题里的深坑」
c语言·汇编·数据结构·c++·笔记·python·算法
abc light14 小时前
ARM 裸机开发(1)——ARM 体系结构
c语言·arm开发
Navigator_Z14 小时前
LeetCode //C - 1309. Decrypt String from Alphabet to Integer Mapping
c语言·算法·leetcode
爱聊灵光的维C15 小时前
C语言初阶:关于函数设计的详细讲解和注意事项
c语言·开发语言·青少年编程·蓝桥杯·学习方法
wuminyu16 小时前
JVM规范学习总结:class执行性能差异
java·linux·c语言·jvm·c++