14. socket网络编程基础.md 10 KB

Socket网络编程基础

1. Socket概念

Socket(套接字) 是网络通信的端点,是操作系统提供给应用程序的网络编程接口。

核心特性

  • 文件描述符抽象:Socket被抽象为文件描述符,可以像操作文件一样使用 read()/write() 进行网络通信
  • 双向通信端点:每个Socket包含发送缓冲区和接收缓冲区
  • 协议无关性:统一的API支持TCP、UDP等不同协议

Socket类型

类型 说明 用途
SOCK_STREAM 面向连接的可靠字节流 TCP通信
SOCK_DGRAM 无连接的数据报 UDP通信
SOCK_RAW 原始套接字 自定义协议、网络诊断

2. Socket地址结构

2.1 通用地址结构 struct sockaddr

struct sockaddr {
    sa_family_t    sa_family;    // 地址族(AF_INET/AF_INET6)
    char           sa_data[14];  // 协议特定地址数据
};

2.2 IPv4地址结构 struct sockaddr_in

struct sockaddr_in {
    sa_family_t    sin_family;   // 地址族,必须是AF_INET
    in_port_t      sin_port;     // 端口号(网络字节序)
    struct in_addr sin_addr;     // IPv4地址
    char           sin_zero[8];  // 填充字节,使大小与sockaddr相同
};

struct in_addr {
    in_addr_t s_addr;            // 32位IPv4地址(网络字节序)
};

2.3 IPv6地址结构 struct sockaddr_in6

struct sockaddr_in6 {
    sa_family_t     sin6_family;    // AF_INET6
    in_port_t       sin6_port;      // 端口号
    uint32_t        sin6_flowinfo;  // 流信息
    struct in6_addr sin6_addr;      // IPv6地址
    uint32_t        sin6_scope_id;  // 范围ID
};

struct in6_addr {
    uint8_t s6_addr[16];           // 128位IPv6地址
};

地址结构使用要点

// 常见错误:直接传递sockaddr_in
int ret = bind(sockfd, (struct sockaddr_in*)&addr, sizeof(addr));  // 错误!

// 正确做法:强制类型转换为sockaddr
int ret = bind(sockfd, (struct sockaddr*)&addr, sizeof(addr));     // 正确!

3. 字节序转换

字节序问题:不同CPU架构使用不同的字节存储顺序

  • 小端序(Little-Endian):低字节在低地址(x86/x64)
  • 大端序(Big-Endian):高字节在低地址(网络字节序)

转换函数

函数 说明 使用场景
htons() 主机→网络,16位 端口号
htonl() 主机→网络,32位 IPv4地址
ntohs() 网络→主机,16位 解析端口号
ntohl() 网络→主机,32位 解析IPv4地址

示例

#include <arpa/inet.h>

uint16_t port = 8080;
uint16_t net_port = htons(port);      // 主机→网络

uint32_t ip = 0xC0A80101;            // 192.168.1.1
uint32_t net_ip = htonl(ip);         // 主机→网络

4. 地址转换函数

4.1 传统函数(IPv4专用)

// 字符串 → 网络字节序二进制
in_addr_t inet_addr(const char *cp);

// 网络字节序 → 字符串
char *inet_ntoa(struct in_addr in);

4.2 现代函数(协议无关)

// 字符串 → 网络字节序二进制(推荐使用)
int inet_pton(int af, const char *src, void *dst);

// 网络字节序 → 字符串(推荐使用)
const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);

示例对比

#include <arpa/inet.h>

// 传统方式
struct in_addr addr;
addr.s_addr = inet_addr("192.168.1.1");
char *str = inet_ntoa(addr);

// 现代方式(推荐)
struct in_addr addr;
inet_pton(AF_INET, "192.168.1.1", &addr);
char buf[INET_ADDRSTRLEN];
inet_ntop(AF_INET, &addr, buf, sizeof(buf));

5. Socket编程完整流程

5.1 服务器流程

socket() → bind() → listen() → accept() → read()/write() → close()

5.2 客户端流程

socket() → connect() → write()/read() → close()

API详解

socket() - 创建套接字

#include <sys/socket.h>

int socket(int domain, int type, int protocol);
// domain:   AF_INET(IPv4), AF_INET6(IPv6)
// type:     SOCK_STREAM(TCP), SOCK_DGRAM(UDP)
// protocol: 0(自动选择)
// 返回值:   成功返回文件描述符,失败返回-1

bind() - 绑定地址

int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
// sockfd:   socket返回的文件描述符
// addr:     要绑定的地址结构指针
// addrlen:  地址结构大小
// 返回值:   成功返回0,失败返回-1

listen() - 监听连接

int listen(int sockfd, int backlog);
// sockfd:   已绑定的socket
// backlog:  已完成连接队列的最大长度(通常128)
// 返回值:   成功返回0,失败返回-1

accept() - 接受连接

int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
// sockfd:   监听socket
// addr:     输出参数,客户端地址信息
// addrlen:  输入输出参数,地址结构大小
// 返回值:   新的socket文件描述符(用于与客户端通信)

connect() - 发起连接

int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
// sockfd:   客户端socket
// addr:     服务器地址
// addrlen:  地址结构大小
// 返回值:   成功返回0,失败返回-1

6. 完整代码示例

6.1 简单TCP服务器

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>

#define PORT 8080
#define BUF_SIZE 1024

int main() {
    int server_fd, client_fd;
    struct sockaddr_in server_addr, client_addr;
    socklen_t addr_len = sizeof(client_addr);
    char buf[BUF_SIZE];

    // 1. 创建socket
    server_fd = socket(AF_INET, SOCK_STREAM, 0);
    if (server_fd < 0) {
        perror("socket");
        exit(EXIT_FAILURE);
    }

    // 设置地址复用
    int opt = 1;
    setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));

    // 2. 绑定地址
    memset(&server_addr, 0, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_addr.s_addr = INADDR_ANY;
    server_addr.sin_port = htons(PORT);

    if (bind(server_fd, (struct sockaddr*)&server_addr, sizeof(server_addr)) < 0) {
        perror("bind");
        close(server_fd);
        exit(EXIT_FAILURE);
    }

    // 3. 监听
    if (listen(server_fd, 128) < 0) {
        perror("listen");
        close(server_fd);
        exit(EXIT_FAILURE);
    }

    printf("Server listening on port %d...\n", PORT);

    // 4. 接受连接
    client_fd = accept(server_fd, (struct sockaddr*)&client_addr, &addr_len);
    if (client_fd < 0) {
        perror("accept");
        close(server_fd);
        exit(EXIT_FAILURE);
    }

    printf("Client connected: %s:%d\n",
           inet_ntoa(client_addr.sin_addr),
           ntohs(client_addr.sin_port));

    // 5. 通信
    ssize_t n;
    while ((n = read(client_fd, buf, BUF_SIZE)) > 0) {
        buf[n] = '\0';
        printf("Received: %s", buf);
        write(client_fd, buf, n);  // Echo回客户端
    }

    // 6. 关闭
    close(client_fd);
    close(server_fd);
    return 0;
}

编译命令:

gcc -Wall -o server server.c

运行:

./server

6.2 简单TCP客户端

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>

#define SERVER_IP "127.0.0.1"
#define PORT 8080
#define BUF_SIZE 1024

int main() {
    int sockfd;
    struct sockaddr_in server_addr;
    char buf[BUF_SIZE];

    // 1. 创建socket
    sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (sockfd < 0) {
        perror("socket");
        exit(EXIT_FAILURE);
    }

    // 2. 连接服务器
    memset(&server_addr, 0, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(PORT);
    inet_pton(AF_INET, SERVER_IP, &server_addr.sin_addr);

    if (connect(sockfd, (struct sockaddr*)&server_addr, sizeof(server_addr)) < 0) {
        perror("connect");
        close(sockfd);
        exit(EXIT_FAILURE);
    }

    printf("Connected to server\n");

    // 3. 通信
    printf("Enter message: ");
    fgets(buf, BUF_SIZE, stdin);
    write(sockfd, buf, strlen(buf));

    ssize_t n = read(sockfd, buf, BUF_SIZE);
    if (n > 0) {
        buf[n] = '\0';
        printf("Echo: %s", buf);
    }

    // 4. 关闭
    close(sockfd);
    return 0;
}

编译命令:

gcc -Wall -o client client.c

7. 注意事项

7.1 常见错误

  • 忘记将 sockaddr_in 转换为 sockaddr
  • 字节序未转换导致端口号/IP错误
  • accept() 返回新fd,监听fd继续监听
  • 未检查系统调用返回值

7.2 最佳实践

  • 使用 SO_REUSEADDR 避免地址占用错误
  • 使用 inet_pton()/inet_ntop() 替代旧函数
  • 始终检查返回值并处理错误
  • 使用 select()/poll()/epoll 实现并发服务器

7.3 资源管理

// 避免文件描述符泄漏
int fd = socket(...);
if (fd < 0) {
    perror("socket");
    return -1;
}

// 使用goto统一错误处理(推荐)
if (bind(...) < 0) goto error;
if (listen(...) < 0) goto error;
// ...
return 0;

error:
    close(fd);
    return -1;

8. 嵌入式网络通信场景

8.1 常见应用

  • 设备间通信:嵌入式设备与服务器数据交互
  • 远程控制:通过网络控制嵌入式设备
  • 日志上传:设备日志发送到远程服务器
  • OTA升级:固件远程更新

8.2 嵌入式环境特殊性

  • 资源受限:注意内存使用,避免动态分配
  • 网络不稳定:实现超时重连机制
  • 字节序问题:嵌入式CPU可能使用小端序
  • 阻塞/非阻塞:根据场景选择合适的I/O模式

9. 面试要点

9.1 核心概念

  • Socket是什么? 与文件描述符的关系
  • 为什么需要字节序转换? 大小端问题
  • sockaddr的作用? 为什么需要通用结构

9.2 API理解

  • socket()/bind()/listen()/accept()/connect()的作用
  • accept()返回新fd的原因(支持并发)
  • INADDR_ANY的含义(绑定所有本地接口)

9.3 常见问题

  • 服务器启动失败: 端口被占用、权限不足
  • 连接失败: 防火墙、IP/端口错误
  • 数据不完整: TCP字节流特性,需要应用层协议

9.4 扩展知识

  • select()/poll()/epoll()的区别
  • 非阻塞I/O与异步I/O
  • SO_REUSEADDRSO_REUSEPORT的区别

相关链接

  • [[15. TCP编程]]
  • [[socket网络编程基础]]