套接字基础:从概念到实战
各位同学,今天我们来聊聊网络编程的基石——套接字。说实话,我刚开始学网络编程时,也被各种概念搞得晕头转向。但后来我发现,只要抓住几个核心点,套接字其实没那么神秘。
套接字到底是什么?
套接字,英文叫 socket。你可以把它想象成「网络世界的文件描述符」。我习惯这么理解:你在 Linux 下操作文件,用的是文件描述符;操作网络连接,用的就是套接字。
套接字本质上是一个编程接口。它屏蔽了底层网络协议的复杂性,让你能像读写文件一样,在网络间收发数据。嗯,这里要注意:套接字不是协议,它是协议的使用接口。
核心要点:套接字是应用层与传输层之间的桥梁。你写网络程序,本质上就是在操作套接字。
字节序:大端与小端
这个问题,我在项目中踩过坑。有一次调试一个跨平台通信的程序,数据死活对不上。查了半天,原来是字节序搞反了。
字节序,说白了就是数据在内存中怎么排列。举个例子,一个 32 位整数 0x12345678:
| 字节序 | 内存排列(低地址 → 高地址) | 常见平台 |
|---|---|---|
| 大端(Big-Endian) | 0x12 0x34 0x56 0x78 | 网络字节序、PowerPC |
| 小端(Little-Endian) | 0x78 0x56 0x34 0x12 | x86、ARM(默认) |
你想想看,如果发送端是小端,接收端是大端,那数据就全乱了。所以网络协议规定:网络字节序统一使用大端。这就是为什么我们总要用 htonl、htons 这些函数做转换。
个人习惯:我写网络程序时,只要涉及多字节数据,一律显式转换。哪怕是在同一台机器上测试,也绝不偷懒。这个习惯帮我避免了很多线上故障。
IP地址转换函数
我们平时写的 IP 地址是点分十进制,比如 "192.168.1.1"。但计算机不认识这个,它要的是 32 位二进制数。所以需要转换函数来帮忙。
常用的几个函数:
#include <arpa/inet.h>
// 字符串 → 网络字节序的二进制
int inet_pton(int af, const char *src, void *dst);
// 示例:inet_pton(AF_INET, "192.168.1.1", &addr.sin_addr);
// 网络字节序的二进制 → 字符串
const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);
// 示例:inet_ntop(AF_INET, &addr.sin_addr, buf, sizeof(buf));
我个人推荐用 inet_pton/inet_ntop 这对函数。它们支持 IPv4 和 IPv6,而且更安全。老版的 inet_addr、inet_ntoa 虽然简单,但有不少限制,我建议你别用了。
避坑指南:我曾经在代码里直接用 inet_addr 转换 IP,结果传入 "0.0.0.0" 时返回了 INADDR_ANY 的语义,导致绑定行为异常。换成 inet_pton 后,问题就解决了。
核心 API:socket / connect / bind / listen / accept
这五个函数,是 TCP 网络编程的骨架。我画了一张图,帮你理清它们的关系:
这张图展示了典型的 TCP 通信流程。服务器端先启动,客户端后连接。我一个个来讲:
1. socket() — 创建套接字
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
// 示例:int sockfd = socket(AF_INET, SOCK_STREAM, 0);
参数说明:
- domain:协议族,AF_INET 表示 IPv4,AF_INET6 表示 IPv6
- type:套接字类型,SOCK_STREAM 是 TCP,SOCK_DGRAM 是 UDP
- protocol:通常传 0,让系统自动选择
返回值是一个文件描述符。如果返回 -1,说明创建失败。我记得有一次排查问题,发现 socket 返回了 -1,一查是进程的文件描述符上限用完了。
2. bind() — 绑定地址
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
// 示例:
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(8080);
addr.sin_addr.s_addr = INADDR_ANY;
bind(sockfd, (struct sockaddr *)&addr, sizeof(addr));
bind 的作用是把套接字和具体的 IP、端口绑定。注意两点:
- 端口号要用 htons 转换到网络字节序
- IP 地址用 INADDR_ANY 表示绑定所有网卡
注意:bind 失败最常见的原因是端口被占用。我曾经在调试时频繁重启程序,结果遇到 "Address already in use" 错误。这时候可以用 setsockopt 设置 SO_REUSEADDR 选项来避免。
3. listen() — 监听连接
int listen(int sockfd, int backlog);
// 示例:listen(sockfd, 5);
listen 把主动套接字变成被动套接字,开始等待客户端连接。backlog 参数指定连接队列的最大长度。我一般设成 5 或 10,具体看业务场景。
4. accept() — 接受连接
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
// 示例:
struct sockaddr_in client_addr;
socklen_t len = sizeof(client_addr);
int connfd = accept(sockfd, (struct sockaddr *)&client_addr, &len);
accept 会阻塞,直到有客户端连接进来。它返回一个新的套接字描述符,用于和这个客户端通信。原来的监听套接字继续等待其他连接。
这里有个细节:accept 返回的 connfd 才是真正用来收发数据的。监听套接字只负责「接客」,不负责「聊天」。
5. connect() — 发起连接
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
// 示例:
struct sockaddr_in server_addr;
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(8080);
inet_pton(AF_INET, "127.0.0.1", &server_addr.sin_addr);
connect(sockfd, (struct sockaddr *)&server_addr, sizeof(server_addr));
connect 是客户端用的。它向服务器发起 TCP 三次握手。如果连接失败,返回 -1。常见失败原因:服务器没启动、端口不对、防火墙拦截。
调试技巧:我习惯在 connect 前后加打印,输出 errno 的值。比如 "Connection refused" 对应 ECONNREFUSED,说明服务器没在监听。"Operation timed out" 对应 ETIMEDOUT,说明网络不通。
一个完整的例子
说了这么多,不如看个完整的 echo 服务器代码。这个例子我经常在课程里用,简单但完整:
// 服务器端
#include <stdio.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
int main() {
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(8888);
addr.sin_addr.s_addr = INADDR_ANY;
bind(sockfd, (struct sockaddr *)&addr, sizeof(addr));
listen(sockfd, 5);
printf("Server listening on port 8888...\n");
struct sockaddr_in client;
socklen_t len = sizeof(client);
int connfd = accept(sockfd, (struct sockaddr *)&client, &len);
char buf[1024];
int n = read(connfd, buf, sizeof(buf));
write(connfd, buf, n);
close(connfd);
close(sockfd);
return 0;
}
// 客户端
#include <stdio.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
int main() {
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(8888);
inet_pton(AF_INET, "127.0.0.1", &addr.sin_addr);
connect(sockfd, (struct sockaddr *)&addr, sizeof(addr));
char *msg = "Hello, server!";
write(sockfd, msg, 14);
char buf[1024] = {0};
read(sockfd, buf, sizeof(buf));
printf("Received: %s\n", buf);
close(sockfd);
return 0;
}
先启动服务器,再启动客户端。客户端发送 "Hello, server!",服务器原样返回。这就是最简单的 TCP 通信。
总结一下:套接字编程的核心就是这五个函数。socket 创建管道,bind 绑定地址,listen 准备接客,accept 接客,connect 上门。理解了这些,你就掌握了 TCP 网络编程的骨架。
好了,这一章的内容就到这里。记住,多写代码,多调试,才能真正理解这些概念。下次见。