1. Socket 基础回顾:从零开始理解网络通信的基石
各位同学,咱们今天正式开讲。在深入 Socket 复用技术之前,我觉得有必要先把地基夯实。Socket 这个东西,说白了就是网络通信的「门」。你想想看,两台机器要聊天,总得有个入口吧?Socket 就是这个入口。
我个人习惯把 Socket 理解成一个「通信管道」的两端。一端在你手里,另一端在服务器手里。数据从这头塞进去,从那头吐出来。嗯,就这么简单。但真正用好它,你得先搞懂几个核心概念。
1.1 什么是 Socket?
Socket 的中文翻译叫「套接字」,听起来挺拗口。我更愿意叫它「网络文件描述符」。为什么?因为 Linux 的设计哲学是「一切皆文件」。网络连接也不例外。
你创建一个 Socket,系统会返回一个整数。这个整数就是文件描述符。你往这个描述符里写数据,就是发送网络请求;你从里面读数据,就是接收网络响应。是不是很像文件操作?
核心定义:Socket 是应用层与传输层之间的抽象接口。它屏蔽了底层网络协议的复杂性,让开发者可以用读写文件的方式操作网络通信。
我记得刚入行那会儿,有个同事死活不理解为什么 Socket 返回的是个 int。他以为应该是个对象。后来我给他画了个图:
1.2 Socket 通信模型
Socket 通信模型,我习惯把它拆成三个角色:客户端、服务器、协议栈。客户端主动发起连接,服务器被动等待连接,协议栈负责把数据打包拆包。
典型的流程是这样的:
- 服务器先启动,创建一个 Socket,绑定 IP 和端口,然后开始监听。
- 客户端后启动,创建一个 Socket,向服务器的 IP:端口发起连接。
- 三次握手(TCP 场景),建立连接。
- 数据传输,双方通过 read/write 交换数据。
- 四次挥手,关闭连接。
这里面有个细节,我当年踩过坑。服务器端的 Socket 其实有两个:一个是监听 Socket,一个是已连接 Socket。监听 Socket 只负责接收新连接,不负责数据传输。已连接 Socket 才是真正干活的。
我曾经犯过的错:刚学 Socket 时,我试图用监听 Socket 去读写数据。结果 read 一直返回 -1,errno 是 ENOTCONN。后来才明白,监听 Socket 只能 accept,不能 read/write。这个坑,你们别踩。
1.3 TCP vs UDP:选哪个?
这个问题,面试官特别喜欢问。我直接给你一张对比表,一目了然:
| 对比维度 | TCP | UDP |
|---|---|---|
| 连接方式 | 面向连接(三次握手) | 无连接(直接发) |
| 可靠性 | 可靠传输(重传、确认) | 不可靠(丢包不重传) |
| 数据顺序 | 保证顺序 | 不保证顺序 |
| 传输速度 | 相对慢(有开销) | 快(无开销) |
| 数据边界 | 流式(无边界) | 报文(有边界) |
| 典型场景 | HTTP、FTP、SSH | DNS、视频直播、游戏 |
说白了,TCP 像快递——保价、签收、有回执。UDP 像明信片——写了就寄,丢了就丢了。你想想看,银行转账能用 UDP 吗?肯定不行。但视频直播用 TCP 呢?卡顿重传,画面反而更差。
我的建议:如果你不确定选哪个,默认选 TCP。等性能瓶颈出现时,再考虑 UDP。我在项目中见过太多人一开始就选 UDP,结果自己实现重传和排序,比 TCP 还慢。
1.4 Socket 编程核心 API
这部分是实操重点。我直接给你一个最简的 TCP 服务端和客户端代码,然后逐行解释。
服务端代码(TCP)
// 1. 创建 Socket
int listen_fd = socket(AF_INET, SOCK_STREAM, 0);
if (listen_fd < 0) {
perror("socket");
exit(1);
}
// 2. 绑定 IP 和端口
struct sockaddr_in server_addr;
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
server_addr.sin_port = htons(8080);
if (bind(listen_fd, (struct sockaddr*)&server_addr, sizeof(server_addr)) < 0) {
perror("bind");
exit(1);
}
// 3. 开始监听
if (listen(listen_fd, 10) < 0) {
perror("listen");
exit(1);
}
// 4. 接受连接
struct sockaddr_in client_addr;
socklen_t client_len = sizeof(client_addr);
int conn_fd = accept(listen_fd, (struct sockaddr*)&client_addr, &client_len);
if (conn_fd < 0) {
perror("accept");
exit(1);
}
// 5. 读写数据
char buffer[1024];
int n = read(conn_fd, buffer, sizeof(buffer));
write(conn_fd, "Hello from server", 17);
// 6. 关闭连接
close(conn_fd);
close(listen_fd);
客户端代码(TCP)
// 1. 创建 Socket
int sock_fd = socket(AF_INET, SOCK_STREAM, 0);
if (sock_fd < 0) {
perror("socket");
exit(1);
}
// 2. 连接服务器
struct sockaddr_in server_addr;
memset(&server_addr, 0, sizeof(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);
if (connect(sock_fd, (struct sockaddr*)&server_addr, sizeof(server_addr)) < 0) {
perror("connect");
exit(1);
}
// 3. 读写数据
write(sock_fd, "Hello from client", 17);
char buffer[1024];
int n = read(sock_fd, buffer, sizeof(buffer));
// 4. 关闭连接
close(sock_fd);
这里有几个关键点,我重点说一下:
- socket():第一个参数 AF_INET 表示 IPv4,SOCK_STREAM 表示 TCP。如果是 UDP,用 SOCK_DGRAM。
- bind():把 Socket 和 IP:端口绑定。服务器必须 bind,客户端一般不用。
- listen():把主动 Socket 变成被动监听 Socket。第二个参数是 backlog,表示最大等待连接数。
- accept():从等待队列里取出一个连接。注意,它返回的是新的 Socket 描述符。
- connect():客户端主动发起连接。如果服务器没在 listen,connect 会失败。
避坑指南:bind 之前一定要用 memset 清零结构体。我见过有人忘了清零,结果 sin_zero 字段里有垃圾数据,bind 返回 EADDRINUSE。还有,端口号要用 htons 转换字节序。网络字节序是大端,x86 是小端,不转换就等着踩坑吧。
1.5 本章小结
好了,第一章的内容就这些。我们回顾一下:
- Socket 是网络通信的抽象接口,本质是文件描述符。
- 通信模型分客户端和服务器,服务器有监听 Socket 和已连接 Socket 之分。
- TCP 可靠但慢,UDP 快但不可靠。选型看场景。
- 核心 API 就六个:socket、bind、listen、accept、connect、close。