socket编程

🌟 一、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为例)

  1. 创建Socketsocket()
  2. 绑定地址和端口bind()
  3. 监听连接listen()
  4. 接受连接accept()
  5. 数据传输send()recv()
  6. 关闭Socketclosesocket()

📱 客户端编程步骤(以TCP为例)

  1. 创建Socketsocket()
  2. 连接服务器connect()
  3. 数据传输send()recv()
  4. 关闭Socketclosesocket()

🔍 四、关键函数详解

1. socket() - 创建Socket

复制代码
#include <sys/types.h>
#include <sys/socket.h>

int socket(int domain, int type, int protocol);

参数说明

  • domain:协议族
    • AF_INET:IPv4
    • AF_INET6:IPv6
    • AF_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和端口
  • addrlenaddr结构的长度

重要结构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. 学习建议

  1. 先学TCP:简单可靠,适合理解基础
  2. 再学UDP:了解无连接通信
  3. 动手实践:写一个简单的聊天程序
  4. 调试技巧:用Wireshark抓包分析网络通信

3. 面试常问问题

  • 为什么TCP需要三次握手?
  • 为什么TCP需要四次挥手?
  • TCP和UDP的适用场景?
  • 如何解决"Address already in use"问题?

🎯 九、最后的鼓励

Socket编程是网络开发的基石,可能一开始会有点难,但一旦掌握了,你就会发现它真的很酷!就像我第一次成功实现一个简单的聊天程序时,那种成就感简直无法形容。

💡 小贴士:在面试中,不要只说"我用过Socket",要说"我理解Socket的工作原理,知道TCP和UDP的区别,能处理常见的错误,比如连接超时、地址占用等。"

相关推荐
热爱编程的OP2 小时前
Linux进程池与管道通信详解:从原理到实现
linux·开发语言·c++
晚风吹长发9 小时前
二分查找算法+题目详解
c++·算法·二分查找
罗义凯10 小时前
其中包含了三种排序算法的注释版本(冒泡排序、选择排序、插入排序),但当前只实现了数组的输入和输出功能。
数据结构·c++·算法
春蕾夏荷_72829772511 小时前
c++ easylogging 使用示例
c++·log·easylogging
syt_biancheng11 小时前
Day3算法训练(简写单词,dd爱框框,3-除2!)
开发语言·c++·算法·贪心算法
自然数e12 小时前
C++多线程【线程管控】之线程转移以及线程数量和ID
开发语言·c++·算法·多线程
Elias不吃糖14 小时前
epoll 事件全集、每个事件的含义、哪些事件在实际服务器中最常见、哪些会组合出现
linux·c++·event
AA陈超14 小时前
ASC学习笔记0017:返回此能力系统组件的所有属性列表
c++·笔记·学习·ue5·虚幻引擎
Unlyrical15 小时前
splice, io_uring_prep_splice 调用(无效参数)
linux·服务器·c++·unix