前言:对于C/C++初学者来说,网络编程似乎是一道"门槛",而Socket就是打开这扇门的钥匙。今天我们一起来看看如何入门Socket网络编程。

目录

一、什么是Socket

二、Socket编程流程

三、TCP Socket编程示例

四、一些注意事项


一、什么是Socket

我们平时在浏览器输入网址,按下回车,为什么能够进入某个网页?为什么我们能够通过本地电脑上的微信或其他聊天APP,与远在他乡的其他伙伴进行交流?这些背后都离不开Socket。

1) Socket概念

Socket(套接字) 是操作系统提供的网络通信编程接口,是应用层与传输层之间的桥梁

┌─────────────────────────────────────┐
│           应用层 (HTTP/FTP/DNS)      
├─────────────────────────────────────┤
│           Socket 接口                 ← 我们编程的位置
├─────────────────────────────────────┤
│        传输层 (TCP/UDP)              
├─────────────────────────────────────┤
│        网络层 (IP/ICMP)              
├─────────────────────────────────────┤
│       网络接口层 (以太网/WiFi)        
└─────────────────────────────────────┘

我们类比一个生活场景:你要打电话给某人,先拨号,那边听到电话铃声后提起电话,这时你和那人就建立起了连接,你们就可以交流了。挂断电话即可结束此次交流。socket其实也是类似的原理。

2) Socket的作用

  • 通信信道:Socket 为网络中的数据传输提供了一个虚拟信道,数据可以通过这个信道从发送方传输到接收方
  • 地址识别:通过 IP 地址和端口号,Socket 能够标识网络中的特定设备和应用程序
  • 协议兼容性:Socket 允许应用使用不同的传输层协议来进行通信,如 TCP 和 UDP。

3) Socket的类型

类型 协议 特点 应用场景
流式套接字 TCP 面向连接、可靠、有序、无边界 文件传输、网页浏览、数据库
数据报套接字 UDP 无连接、不可靠、有边界、高效 视频直播、DNS、在线游戏
原始套接字 IP/ICMP 直接访问底层协议 网络诊断、安全工具

二、Socket编程流程

1) 服务端编程步骤

  1. 创建套接字(socket):使用socket()函数创建一个新的套接字。

  2. 绑定套接字(bind):通过bind()函数将套接字与特定的IP地址和端口号关联起来。

  3. 监听连接(listen):使用listen()函数使服务器套接字监听来自客户端的连接请求。

  4. 接受连接(accept):当客户端请求连接时,accept()函数会接受这个连接。

  5. 读取数据(read/recv):从客户端接收数据。

  6. 发送数据(write/send):向客户端发送数据。

  7. 关闭套接字(close):完成数据传输后,关闭连接。

2) 客户端编程步骤

  1. 创建套接字(socket):同服务端。

  2. 发起连接(connect):使用connect()函数向服务器发起连接请求。

  3. 发送数据(write/send):向服务器发送数据。

  4. 读取数据(read/recv):从服务器接收数据。

  5. 关闭套接字(close):完成数据传输后,关闭连接。

例如,TCP Socket通信流程:

  服务端(Server)                           客户端(Client)
      │                                          │
      ▼                                          ▼
┌─────────────┐                           ┌─────────────┐
│  socket()   │  创建套接字                │  socket()   │
│  创建监听fd  │                           │  创建通信fd  |
└──────┬──────┘                           └──────┬──────┘
       │                                         │
       ▼                                         │
┌─────────────┐                                  │
│   bind()    │  绑定IP:端口                      │
│ 0.0.0.0:8080│                                  │
└──────┬──────┘                                  │
       │                                         │
       ▼                                         │
┌─────────────┐                                  │
│  listen()   │  开始监听(backlog队列)           │
│  被动等待连接│                                  │
└──────┬──────┘                                  │
       │◄────────────────────────────────────────┤
       │         connect() 发起连接               │
       │         三次握手开始...                  │
       ▼                                         ▼
┌─────────────┐                           ┌─────────────┐
│  accept()   │◄───────────────────────►  │   连接建立   │
│ 阻塞等待连接 │  返回新的通信fd(connfd)    │             │
└──────┬──────┘                           └──────┬──────┘
       │                                         │
       │◄────────── 双向数据传输 ───────────────► │
       │     read()/write() 或 recv()/send()     │
       │                                         │
       │         四次挥手断开连接...              │
       │◄───────────────────────────────────────►│
       ▼                                         ▼
┌─────────────┐                         ┌─────────────┐
│   close()   │                         │   close()   │
│ 关闭connfd  │                         │   关闭fd     │
│ 关闭listenfd│                         │              │
└─────────────┘                         └─────────────┘

三、TCP Socket编程示例

为方便理解概念,这里仅演示基础版本示例 — 阻塞式TCP通信(单线程)。

服务器编程:

// server_basic.cpp
#include <iostream>
#include <cstring>
#include <string>
#include <unistd.h>      // close, read, write
#include <arpa/inet.h>   // inet_addr, htons
#include <sys/socket.h>  // socket, bind, listen, accept

int main() {
    // 1. 创建套接字 (IPv4, TCP, 默认协议)
    int server_fd = socket(AF_INET, SOCK_STREAM, 0);
    if (server_fd == -1) {
        std::cerr << "创建socket失败" << std::endl;
        return -1;
    }
    std::cout << "[1] Socket创建成功, fd=" << server_fd << std::endl;

    // 2. 绑定地址和端口
    struct sockaddr_in server_addr;
    memset(&server_addr, 0, sizeof(server_addr));  // 清零
    server_addr.sin_family = AF_INET;              // IPv4
    server_addr.sin_port = htons(8080);           // 端口8080 (转网络字节序)
    server_addr.sin_addr.s_addr = INADDR_ANY;     // 监听所有网卡 0.0.0.0

    if (bind(server_fd, (struct sockaddr*)&server_addr, sizeof(server_addr)) == -1) {
        std::cerr << "绑定失败" << std::endl;
        close(server_fd);
        return -1;
    }
    std::cout << "[2] 绑定成功 0.0.0.0:8080" << std::endl;

    // 3. 开始监听 (最大等待队列长度5)
    if (listen(server_fd, 5) == -1) {
        std::cerr << "监听失败" << std::endl;
        close(server_fd);
        return -1;
    }
    std::cout << "[3] 开始监听..." << std::endl;

    // 4. 接受客户端连接 (阻塞等待)
    struct sockaddr_in client_addr;
    socklen_t addr_len = sizeof(client_addr);
    std::cout << "[4] 等待客户端连接..." << std::endl;
    
    int client_fd = accept(server_fd, (struct sockaddr*)&client_addr, &addr_len);
    if (client_fd == -1) {
        std::cerr << "接受连接失败" << std::endl;
        close(server_fd);
        return -1;
    }
    
    char client_ip[16];
    inet_ntop(AF_INET, &client_addr.sin_addr, client_ip, sizeof(client_ip));
    std::cout << "[5] 客户端连接! IP=" << client_ip 
              << " 端口=" << ntohs(client_addr.sin_port) << std::endl;

    // 5. 数据收发循环
    char buffer[1024];
    while (true) {
        // 接收数据
        memset(buffer, 0, sizeof(buffer));
        int recv_len = recv(client_fd, buffer, sizeof(buffer) - 1, 0);
        
        if (recv_len <= 0) {
            std::cout << "[6] 客户端断开连接" << std::endl;
            break;
        }
        
        std::cout << "[收到] " << buffer << std::endl;

        // 发送响应
        std::string response = "服务器收到: ";
        response += buffer;
        send(client_fd, response.c_str(), response.length(), 0);
    }

    // 6. 关闭连接
    close(client_fd);
    close(server_fd);
    std::cout << "[7] 服务器关闭" << std::endl;

    return 0;
}

客户端编程:

// client_basic.cpp
#include <iostream>
#include <cstring>
#include <string>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>

int main() {
    // 1. 创建套接字
    int sock_fd = socket(AF_INET, SOCK_STREAM, 0);
    if (sock_fd == -1) {
        std::cerr << "创建socket失败" << std::endl;
        return -1;
    }
    std::cout << "[1] Socket创建成功" << std::endl;

    // 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);  // 服务器端口
    
    // 将IP字符串转为网络字节序
    if (inet_pton(AF_INET, "127.0.0.1", &server_addr.sin_addr) <= 0) {
        std::cerr << "无效的IP地址" << std::endl;
        close(sock_fd);
        return -1;
    }

    // 3. 连接服务器
    std::cout << "[2] 正在连接服务器..." << std::endl;
    if (connect(sock_fd, (struct sockaddr*)&server_addr, sizeof(server_addr)) == -1) {
        std::cerr << "连接失败" << std::endl;
        close(sock_fd);
        return -1;
    }
    std::cout << "[3] 连接成功!" << std::endl;

    // 4. 发送和接收数据
    char buffer[1024];
    while (true) {
        // 输入消息
        std::cout << "请输入消息 (输入quit退出): ";
        std::string msg;
        std::getline(std::cin, msg);

        if (msg == "quit") {
            break;
        }

        // 发送
        if (send(sock_fd, msg.c_str(), msg.length(), 0) == -1) {
            std::cerr << "发送失败" << std::endl;
            break;
        }

        // 接收响应
        memset(buffer, 0, sizeof(buffer));
        int recv_len = recv(sock_fd, buffer, sizeof(buffer) - 1, 0);
        if (recv_len <= 0) {
            std::cout << "服务器断开连接" << std::endl;
            break;
        }

        std::cout << "[服务器回复] " << buffer << std::endl;
    }

    // 5. 关闭
    close(sock_fd);
    std::cout << "[4] 客户端关闭" << std::endl;

    return 0;
}

四、一些注意事项

1) 网络字节序问题

一般来说,端口号、IP 地址在协议头中都是网络字节序(大端模式),如果我们直接按照主机字节序发送,那么,在大小端不同的机器上就会发生数据错乱。

2) 返回值与错误码

每个系统调用都需要检查返回值,并且打印相关错误信息,防止因为传入参数异常等原因而发生非预期行为,方便定位问题。

3) 必须关闭Socket

编程过程中,我们要养成良好的习惯,最后结束的时候一定要关闭socket。如果不关闭,文件描述符或句柄还存在系统中,资源会被慢慢耗尽。

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐