# 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` ```c struct sockaddr { sa_family_t sa_family; // 地址族(AF_INET/AF_INET6) char sa_data[14]; // 协议特定地址数据 }; ``` ### 2.2 IPv4地址结构 `struct sockaddr_in` ```c 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` ```c 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地址 }; ``` ### 地址结构使用要点 ```c // 常见错误:直接传递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地址 | ### 示例 ```c #include 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专用) ```c // 字符串 → 网络字节序二进制 in_addr_t inet_addr(const char *cp); // 网络字节序 → 字符串 char *inet_ntoa(struct in_addr in); ``` ### 4.2 现代函数(协议无关) ```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); ``` ### 示例对比 ```c #include // 传统方式 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() - 创建套接字 ```c #include 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() - 绑定地址 ```c int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen); // sockfd: socket返回的文件描述符 // addr: 要绑定的地址结构指针 // addrlen: 地址结构大小 // 返回值: 成功返回0,失败返回-1 ``` #### listen() - 监听连接 ```c int listen(int sockfd, int backlog); // sockfd: 已绑定的socket // backlog: 已完成连接队列的最大长度(通常128) // 返回值: 成功返回0,失败返回-1 ``` #### accept() - 接受连接 ```c int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen); // sockfd: 监听socket // addr: 输出参数,客户端地址信息 // addrlen: 输入输出参数,地址结构大小 // 返回值: 新的socket文件描述符(用于与客户端通信) ``` #### connect() - 发起连接 ```c int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen); // sockfd: 客户端socket // addr: 服务器地址 // addrlen: 地址结构大小 // 返回值: 成功返回0,失败返回-1 ``` --- ## 6. 完整代码示例 ### 6.1 简单TCP服务器 ```c #include #include #include #include #include #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; } ``` **编译命令:** ```bash gcc -Wall -o server server.c ``` **运行:** ```bash ./server ``` ### 6.2 简单TCP客户端 ```c #include #include #include #include #include #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; } ``` **编译命令:** ```bash 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 资源管理 ```c // 避免文件描述符泄漏 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_REUSEADDR`与`SO_REUSEPORT`的区别 --- ## 相关链接 - [[15. TCP编程]] - [[socket网络编程基础]]