Socket 编程基础:从一根网线说起
说实话,我刚开始学网络编程那会儿,对 Socket 的理解特别模糊。总觉得它是个很玄乎的东西,像什么「套接字」——这翻译就让人摸不着头脑。后来踩了不少坑,才慢慢搞明白:Socket 其实就是操作系统提供的一个编程接口,让你能通过代码收发网络数据。
打个比方吧。你想跟远方的朋友打电话,得先有部电话机吧?Socket 就是那部电话机。你拨号、接通、说话、挂断,每一步操作系统都帮你封装好了。你只需要拿着这个「电话机」去操作就行。
核心一句话:Socket 是应用层与传输层之间的桥梁。它屏蔽了底层 TCP/IP 协议的复杂性,让你用 read/write 就能收发网络数据。
Socket 到底是什么?
从定义上讲,Socket 是一个文件描述符。对,你没看错——在 Linux 世界里,一切皆文件。网络连接也不例外。
我当年第一次看到这个说法时也很困惑:「网络连接怎么就成了文件?」后来写了个简单的 echo 服务器才明白:你创建一个 Socket,得到一个 fd(文件描述符),然后对这个 fd 做 read 和 write,数据就在网络上传输了。跟读写本地文件几乎一样。
从编程角度看,Socket 就是一组 API 的集合:
socket()—— 创建一部「电话机」bind()—— 给电话机绑定一个号码(IP + 端口)listen()—— 把电话机调到待机状态accept()—— 接听来电connect()—— 拨号出去send() / recv()—— 通话,说和听close()—— 挂断电话
嗯,这套流程我闭着眼都能背出来。因为我在生产环境里写过不下几十次。
Socket 的两种主要类型
Socket 分两种,对应传输层的两大协议:TCP 和 UDP。它们的区别,说白了就是一个靠谱一个快。
| 特性 | 流式 Socket (SOCK_STREAM) | 数据报 Socket (SOCK_DGRAM) |
|---|---|---|
| 底层协议 | TCP | UDP |
| 连接方式 | 面向连接,先建立连接再通信 | 无连接,直接发数据 |
| 数据可靠性 | 保证到达,顺序一致 | 不保证到达,可能乱序 |
| 数据边界 | 无边界,流式数据 | 有边界,每个包独立 |
| 典型场景 | HTTP、FTP、邮件 | DNS、视频直播、游戏 |
流式 Socket(SOCK_STREAM)
这种 Socket 基于 TCP,特点是可靠、有序、面向连接。你发出去的数据,TCP 保证对方能收到,而且顺序不乱。
但有个坑:它是流式的,没有消息边界。什么意思呢?你调用两次 send() 各发了 100 字节,对方可能一次 recv() 就收回了 200 字节。也可能分三次收:50、100、50。边界在哪?你自己得想办法。
我曾经踩过的坑:写第一个 HTTP 解析器时,我天真地以为一次 recv() 就能收完整个 HTTP 请求头。结果在高并发下频繁出现「半包」问题——请求头只收到一半,解析直接崩了。后来才加上消息边界检测和缓冲区拼接逻辑。
数据报 Socket(SOCK_DGRAM)
这种基于 UDP,特点是快速、无连接、不可靠。每次 sendto() 就是一个独立的数据报,对方 recvfrom() 收到的就是完整的一包。
好处是简单——不用维护连接状态,发就完了。坏处是——丢了就丢了,没人管你。
我个人的习惯是:如果丢一两个包不影响业务,就用 UDP。比如视频通话、游戏位置同步。但如果涉及交易、支付、文件传输,老老实实用 TCP。
Socket 通信模型:C/S 架构
几乎所有 Socket 程序都遵循 Client-Server(客户端-服务器) 模型。这个架构其实很直观:
- Server:一直等着,谁来接谁
- Client:主动发起连接,然后通信
下面这张图是我自己画的标准 C/S 通信流程,你看一眼就明白了:
这个流程里有个细节我特别想强调:服务端必须先启动,然后调用 listen() 进入被动等待状态。客户端才能 connect() 过来。如果服务端没启动,客户端 connect() 会直接返回「连接被拒绝」——这个错误我见过太多次了。
一个小技巧:调试时如果 connect() 报错,先确认服务端进程是不是活着。用 netstat -tlnp | grep 端口号 看一眼,比瞎猜快得多。
Socket 与端口的关系
端口是什么?我习惯这么理解:IP 地址是门牌号,端口是房间号。
一台服务器只有一个 IP,但可以同时跑很多服务:Web 服务、数据库、SSH……每个服务怎么区分?靠端口。HTTP 默认走 80 端口,HTTPS 走 443,MySQL 走 3306。你访问一个网站时,浏览器知道要去 80 号「房间」敲门。
从 Socket 的角度看,端口是这么工作的:
- 服务端调用
bind()时,要指定 IP 和端口。比如bind(sockfd, (struct sockaddr*)&addr, sizeof(addr)),其中addr.sin_port就是端口号。 - 客户端
connect()时,也要指定目标 IP 和端口。比如连接百度:IP 是 14.215.177.39,端口是 80。 - 操作系统通过 五元组 来唯一标识一个连接:
源IP + 源端口 + 目标IP + 目标端口 + 协议。
这里有个常见的误区:端口是 16 位的,范围 0-65535。但 0-1023 是知名端口,需要 root 权限才能绑定。我当年写第一个 Web 服务器时,想用 80 端口,结果 bind() 一直报权限错误——后来才意识到要加 sudo。
我曾经犯过的错:在生产环境里,我直接把服务绑在了 1024 以下的端口上,结果忘了给进程提权。服务启动失败,线上挂了 10 分钟。从那以后,我养成了习惯:低于 1024 的端口一律用 iptables 做端口转发,应用层只用 1024 以上的端口。
一个简单的 TCP Socket 示例
光说不练假把式。我写个最简的 TCP 服务端和客户端,帮你把上面的流程串起来。
服务端代码(server.c):
#include <stdio.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <string.h>
#include <unistd.h>
int main() {
int sockfd, clientfd;
struct sockaddr_in addr;
char buffer[1024] = {0};
// 1. 创建 Socket
sockfd = socket(AF_INET, SOCK_STREAM, 0);
// 2. 绑定地址和端口
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = INADDR_ANY; // 监听所有网卡
addr.sin_port = htons(8080); // 端口 8080
bind(sockfd, (struct sockaddr*)&addr, sizeof(addr));
// 3. 监听
listen(sockfd, 5);
printf("Server listening on port 8080...\n");
// 4. 接受连接
clientfd = accept(sockfd, NULL, NULL);
// 5. 接收数据
recv(clientfd, buffer, sizeof(buffer), 0);
printf("Received: %s\n", buffer);
// 6. 关闭
close(clientfd);
close(sockfd);
return 0;
}
客户端代码(client.c):
#include <stdio.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <string.h>
#include <unistd.h>
int main() {
int sockfd;
struct sockaddr_in addr;
char *message = "Hello, Socket!";
// 1. 创建 Socket
sockfd = socket(AF_INET, SOCK_STREAM, 0);
// 2. 设置目标地址
addr.sin_family = AF_INET;
addr.sin_port = htons(8080);
inet_pton(AF_INET, "127.0.0.1", &addr.sin_addr);
// 3. 连接服务端
connect(sockfd, (struct sockaddr*)&addr, sizeof(addr));
// 4. 发送数据
send(sockfd, message, strlen(message), 0);
printf("Message sent\n");
// 5. 关闭
close(sockfd);
return 0;
}
编译运行:
# 先启动服务端
gcc server.c -o server && ./server
# 另开终端,运行客户端
gcc client.c -o client && ./client
你会看到服务端打印出 "Received: Hello, Socket!"。这就是最基础的 Socket 通信。
小结一下
这一章我们聊了 Socket 的本质——它就是操作系统给应用层的一个网络读写接口。两种类型:流式 Socket 靠谱但没边界,数据报 Socket 快但不靠谱。通信模型就是经典的 C/S 架构,服务端等着,客户端主动连。端口则是用来区分同一台机器上不同服务的「房间号」。
这些概念看着简单,但我在实际项目中见过太多人栽在细节上。比如 bind() 端口被占用、connect() 没加超时、recv() 没处理半包……后面几章我们会一个一个拆解这些问题。