套接字基础:从概念到实战

各位同学,今天我们来聊聊网络编程的基石——套接字。说实话,我刚开始学网络编程时,也被各种概念搞得晕头转向。但后来我发现,只要抓住几个核心点,套接字其实没那么神秘。

套接字到底是什么?

套接字,英文叫 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 套接字编程流程 服务器端 客户端 socket() 创建套接字 bind() 绑定地址和端口 listen() 监听连接 accept() 接受连接 read() / write() 通信 socket() 创建套接字 connect() 发起连接 read() / write() 通信 三次握手 数据双向传输

这张图展示了典型的 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 网络编程的骨架。

好了,这一章的内容就到这里。记住,多写代码,多调试,才能真正理解这些概念。下次见。

公众号:蓝海资料掘金营,微信deep3321