🌟 一、Socket是什么?为什么重要?
Socket(套接字)是网络通信的端点,就像电话的两端。当两台计算机要通信时,它们各自有一个Socket,通过Socket进行数据交换。
简单比喻:想象Socket是两座城市之间的"铁路车站",数据是"列车",而网络协议就是"铁路系统"。
为什么学Socket编程?
- 你写的程序能和互联网上的其他程序通信
- 了解网络工作原理,不再只是"黑盒"使用
- 为开发服务器、客户端、聊天应用打基础
- 面试必问!很多大厂都考这个
📡 二、Socket基础概念
1. Socket的"五元组"
Socket通信需要五个要素:
- 本地IP地址
- 本地端口号
- 远程IP地址
- 远程端口号
- 协议(TCP/UDP)
2. 套接字类型(关键!)
| 类型 | 说明 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|---|
SOCK_STREAM |
面向连接的流式套接字 | 适合可靠传输 | 保证数据顺序、完整性 | 有连接建立过程,稍慢 |
SOCK_DGRAM |
无连接的数据报套接字 | 适合实时应用 | 无连接,速度快 | 不保证顺序和完整性 |
SOCK_RAW |
原始套接字 | 网络诊断、安全工具 | 直接访问底层协议 | 复杂,一般不用 |
💡 重要提示 :C++中通常用
SOCK_STREAM(TCP)和SOCK_DGRAM(UDP)。
3. 通信协议:TCP vs UDP(超重要!)
| 特性 | TCP | UDP |
|---|---|---|
| 连接方式 | 面向连接(需要三次握手) | 无连接 |
| 可靠性 | 高(保证数据完整) | 低(可能丢失) |
| 顺序 | 保证数据顺序 | 不保证顺序 |
| 速度 | 稍慢(有握手、确认机制) | 快(无握手) |
| 适用场景 | 文件传输、网页浏览、邮件 | 视频会议、在线游戏、实时数据 |
| 比喻 | 电话(需要先拨号建立连接) | 信件(直接寄送,不保证送达) |
🌟 面试小技巧:当被问到"为什么用TCP不用UDP"时,可以说:"TCP保证数据完整性和顺序,适合需要可靠传输的场景,如文件传输;UDP速度快,适合实时性要求高的场景,如视频通话。"
🛠 三、Socket编程步骤详解
🖥 服务器端编程步骤(以TCP为例)
- 创建Socket :
socket() - 绑定地址和端口 :
bind() - 监听连接 :
listen() - 接受连接 :
accept() - 数据传输 :
send()和recv() - 关闭Socket :
closesocket()
📱 客户端编程步骤(以TCP为例)
- 创建Socket :
socket() - 连接服务器 :
connect() - 数据传输 :
send()和recv() - 关闭Socket :
closesocket()
🔍 四、关键函数详解
1. socket() - 创建Socket
#include <sys/types.h>
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
参数说明:
domain:协议族AF_INET:IPv4AF_INET6:IPv6AF_UNIX:本地通信(同一台机器上的进程间通信)
type:套接字类型SOCK_STREAM:流式(TCP)SOCK_DGRAM:数据报(UDP)
protocol:协议- 通常为
0(系统自动选择)
- 通常为
返回值:成功返回Socket描述符(非负整数),失败返回-1
💡 示例 :
int sock = socket(AF_INET, SOCK_STREAM, 0);
2. bind() - 绑定地址和端口
#include <sys/types.h>
#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
参数说明:
sockfd:Socket描述符(由socket()返回)addr:指向sockaddr结构的指针,包含IP和端口addrlen:addr结构的长度
重要结构 :sockaddr_in(IPv4)
struct sockaddr_in {
sa_family_t sin_family; // 地址族,AF_INET
in_port_t sin_port; // 端口号(网络字节序)
struct in_addr sin_addr; // IP地址
};
struct in_addr {
uint32_t s_addr; // IP地址(网络字节序)
};
💡 关键点:服务器必须绑定地址和端口,这样客户端才能知道连接哪里。
3. listen() - 设置监听队列
int listen(int sockfd, int backlog);
参数说明:
sockfd:Socket描述符backlog:请求连接队列的最大长度
返回值:成功返回0,失败返回SOCKET_ERROR
💡 重要提示 :
backlog通常设为5-10,表示最多可以有5-10个连接请求排队等待。
4. accept() - 接受客户端连接
SOCKET accept(SOCKET s, struct sockaddr *addr, int *addrlen);
参数说明:
s:已绑定并监听的Socket描述符addr:指向客户端地址结构的指针addrlen:客户端地址结构的长度
返回值:成功返回新的Socket描述符(用于与客户端通信),失败返回INVALID_SOCKET
💡 关键点 :
accept()会创建一个新的Socket用于与客户端通信,服务器主Socket继续监听其他连接。
5. connect() - 连接服务器
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
参数说明:
sockfd:客户端Socket描述符addr:服务器地址结构addrlen:地址结构长度
返回值:成功返回0,失败返回SOCKET_ERROR
6. send()和recv() - 数据传输
// 发送数据
int send(SOCKET s, const char *buf, int len, int flags);
// 接收数据
int recv(SOCKET s, char *buf, int len, int flags);
参数说明:
s:Socket描述符buf:数据缓冲区len:数据长度flags:传输控制标志(通常为0)
返回值:成功返回发送/接收的字节数,失败返回SOCKET_ERROR
💡 重要提示 :
send()和recv()是阻塞的,意味着如果没有数据可读/可写,它们会等待。
🧪 五、完整代码示例(TCP)
1. 服务器端代码
#include <iostream>
#include <cstring>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
int main() {
// 1. 创建Socket
int serverSocket = socket(AF_INET, SOCK_STREAM, 0);
if (serverSocket == -1) {
std::cerr << "Socket creation failed" << std::endl;
return 1;
}
// 2. 绑定地址和端口
struct sockaddr_in serverAddr;
serverAddr.sin_family = AF_INET;
serverAddr.sin_port = htons(8080); // 端口号
serverAddr.sin_addr.s_addr = INADDR_ANY; // 监听所有IP
if (bind(serverSocket, (struct sockaddr*)&serverAddr, sizeof(serverAddr)) == -1) {
std::cerr << "Bind failed" << std::endl;
return 1;
}
// 3. 监听连接
if (listen(serverSocket, 5) == -1) {
std::cerr << "Listen failed" << std::endl;
return 1;
}
std::cout << "Server listening on port 8080..." << std::endl;
// 4. 接受连接
struct sockaddr_in clientAddr;
socklen_t clientAddrSize = sizeof(clientAddr);
int clientSocket = accept(serverSocket, (struct sockaddr*)&clientAddr, &clientAddrSize);
if (clientSocket == -1) {
std::cerr << "Accept failed" << std::endl;
return 1;
}
std::cout << "Client connected!" << std::endl;
// 5. 数据传输
char buffer[1024];
int bytesReceived = recv(clientSocket, buffer, sizeof(buffer), 0);
if (bytesReceived > 0) {
buffer[bytesReceived] = '\0'; // 添加字符串结束符
std::cout << "Received from client: " << buffer << std::endl;
// 发送响应
std::string response = "Hello from server!";
send(clientSocket, response.c_str(), response.size(), 0);
}
// 6. 关闭Socket
close(clientSocket);
close(serverSocket);
return 0;
}
2. 客户端代码
#include <iostream>
#include <cstring>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
int main() {
// 1. 创建Socket
int clientSocket = socket(AF_INET, SOCK_STREAM, 0);
if (clientSocket == -1) {
std::cerr << "Socket creation failed" << std::endl;
return 1;
}
// 2. 连接服务器
struct sockaddr_in serverAddr;
serverAddr.sin_family = AF_INET;
serverAddr.sin_port = htons(8080); // 端口号
serverAddr.sin_addr.s_addr = inet_addr("127.0.0.1"); // 本地服务器
if (connect(clientSocket, (struct sockaddr*)&serverAddr, sizeof(serverAddr)) == -1) {
std::cerr << "Connection failed" << std::endl;
return 1;
}
std::cout << "Connected to server!" << std::endl;
// 3. 数据传输
std::string message = "Hello from client!";
send(clientSocket, message.c_str(), message.size(), 0);
char buffer[1024];
int bytesReceived = recv(clientSocket, buffer, sizeof(buffer), 0);
if (bytesReceived > 0) {
buffer[bytesReceived] = '\0';
std::cout << "Server response: " << buffer << std::endl;
}
// 4. 关闭Socket
close(clientSocket);
return 0;
}
🔧 六、常见问题与解决方法
1. "Address already in use"错误
原因:端口被占用(可能是上次运行的程序没关)
解决方法:
int opt = 1;
setsockopt(serverSocket, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
2. 字节序问题(网络字节序 vs 主机字节序)
问题:不同计算机的字节序不同(大端/小端)
解决方法 :使用htons()和ntohs()转换
serverAddr.sin_port = htons(8080); // 将主机字节序转换为网络字节序
3. 阻塞与非阻塞模式
阻塞模式:函数等待直到操作完成(默认)
非阻塞模式:函数立即返回,无论操作是否完成
设置非阻塞模式:
fcntl(clientSocket, F_SETFL, O_NONBLOCK);
4. 错误处理
关键:检查每个函数的返回值,不要忽略错误!
if (bind(serverSocket, (struct sockaddr*)&serverAddr, sizeof(serverAddr)) == -1) {
perror("Bind failed");
return 1;
}
💡 七、进阶知识
1. 选择(select)与多路复用
当需要同时处理多个连接时,使用select():
fd_set readfds;
FD_ZERO(&readfds);
FD_SET(serverSocket, &readfds);
int maxSocket = serverSocket;
int activity = select(maxSocket + 1, &readfds, NULL, NULL, NULL);
2. 多线程/多进程服务器
- 多线程:每个连接由一个新线程处理
- 多进程:每个连接由一个新进程处理
3. UDP编程
UDP没有连接建立过程,直接发送和接收:
// 服务器
sendto(serverSocket, buffer, len, 0, (struct sockaddr*)&clientAddr, sizeof(clientAddr));
// 客户端
recvfrom(clientSocket, buffer, sizeof(buffer), 0, (struct sockaddr*)&serverAddr, &addrLen);
📌 八、总结与建议
1. Socket编程的核心思想
- 服务器:创建Socket → 绑定 → 监听 → 接受连接 → 通信 → 关闭
- 客户端:创建Socket → 连接服务器 → 通信 → 关闭
2. 学习建议
- 先学TCP:简单可靠,适合理解基础
- 再学UDP:了解无连接通信
- 动手实践:写一个简单的聊天程序
- 调试技巧:用Wireshark抓包分析网络通信
3. 面试常问问题
- 为什么TCP需要三次握手?
- 为什么TCP需要四次挥手?
- TCP和UDP的适用场景?
- 如何解决"Address already in use"问题?
🎯 九、最后的鼓励
Socket编程是网络开发的基石,可能一开始会有点难,但一旦掌握了,你就会发现它真的很酷!就像我第一次成功实现一个简单的聊天程序时,那种成就感简直无法形容。
💡 小贴士:在面试中,不要只说"我用过Socket",要说"我理解Socket的工作原理,知道TCP和UDP的区别,能处理常见的错误,比如连接超时、地址占用等。"