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 *)。举例:
cstruct 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 字节)
规则划分:
- 网络协议头(IP 头、UDP 头):强制大端序 。网络设备统一按大端解析;小端主机若不转大端,IP/端口字节倒序(如
192.168.1.7错乱为7.1.168.192)会导致寻址失败。 - 应用层数据(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) // 恢复默认对齐