socket系统调用及示例

我们继续学习 Linux 系统编程中的重要函数。这次我们介绍 socket 函数,它是网络编程的基础,用于创建一个套接字 (socket),这是进程间通过网络进行通信的端点


1. 函数介绍

socket 是一个 Linux 系统调用,用于创建一个新的**套接字 **(socket)。套接字是一种抽象的概念,它是网络通信的基础端点。你可以把套接字想象成电话的听筒:

  • 你需要先有一个听筒(调用 socket 创建)。
  • 然后你可以用它来拨打电话(connect,作为客户端)或接听电话(bind + listen + accept,作为服务器)。
  • 通过这个听筒,你可以说话(send/write)和听话(recv/read)。

socket 函数本身并不执行网络连接,它只是创建一个通信的“插头”或“接口”,后续需要使用 bindlistenacceptconnectsendrecv 等函数来完成具体的网络操作。


2. 函数原型

#include <sys/socket.h> // 必需
#include <sys/types.h>  // 有时需要

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

3. 功能

  • 创建套接字: 请求内核创建一个新的套接字对象。
  • 指定通信特性: 通过参数定义套接字的**通信域 **(domain)、**类型 (type) 和协议 **(protocol),从而确定套接字的行为和能力。
  • 返回文件描述符: 如果成功,返回一个与新创建的套接字关联的**文件描述符 **(file descriptor)。后续所有对该套接字的操作(如 bindconnectreadwrite)都将使用这个文件描述符。

4. 参数

  • int domain: 指定套接字的通信域,即套接字可以通信的范围。
    • AF_INET: IPv4 Internet 协议域。这是最常用的域,用于通过 IPv4 网络进行通信。
    • AF_INET6: IPv6 Internet 协议域。用于通过 IPv6 网络进行通信。
    • AF_UNIX 或 AF_LOCAL: Unix 域套接字。用于同一台机器上进程间的本地通信,不涉及网络协议栈,非常高效。
    • AF_PACKET: 用于直接访问网络接口(数据链路层)。
    • AF_NETLINK: 用于与内核进行通信。
  • int type: 指定套接字的通信语义类型
    • SOCK_STREAM流式套接字。提供面向连接可靠有序的双向数据传输。TCP 协议就是基于流式套接字的。数据像水流一样,没有边界。
    • SOCK_DGRAM数据报套接字。提供无连接不可靠(可能丢包、重复、乱序)、有边界的数据传输。UDP 协议就是基于数据报套接字的。数据以一个独立的“包裹”(数据报)形式发送。
    • SOCK_RAW原始套接字。允许直接访问底层协议(如 IP 或 ICMP)。通常需要 root 权限。
    • SOCK_SEQPACKET有序数据包套接字。提供面向连接、可靠、有边界有序的数据传输(像 TCP 一样可靠有序,但像 UDP 一样有消息边界)。
    • 修饰符 (可以按位或 | 到 type 上):
      • SOCK_NONBLOCK: 将套接字设置为非阻塞模式。等同于创建套接字后调用 fcntl(sock, F_SETFL, O_NONBLOCK)
      • SOCK_CLOEXEC: 在调用 exec() 时自动关闭该套接字。等同于创建后调用 fcntl(sock, F_SETFD, FD_CLOEXEC)。这可以防止将套接字意外传递给新执行的程序。
  • int protocol: 指定在给定域和类型下使用的具体协议
    • 在大多数情况下,对于 AF_INET 和 AF_INET6
      • 如果 type 是 SOCK_STREAM,则 protocol 通常为 IPPROTO_TCP (或 0,内核会选择默认协议 TCP)。
      • 如果 type 是 SOCK_DGRAM,则 protocol 通常为 IPPROTO_UDP (或 0,内核会选择默认协议 UDP)。
    • 通常设置为 0,表示使用给定 domain 和 type 的默认协议
    • 对于原始套接字 (SOCK_RAW),需要显式指定协议,如 IPPROTO_ICMPIPPROTO_RAW 等。

5. 返回值

  • 成功时: 返回一个非负整数,即新创建套接字的**文件描述符 **(file descriptor)。
  • 失败时: 返回 -1,并设置全局变量 errno 来指示具体的错误原因(例如 EACCES 权限不足,EAFNOSUPPORT 不支持的地址族,EINVAL 无效参数,EMFILE 进程打开的文件描述符已达上限,ENFILE 系统打开的文件总数已达上限,ENOMEM 内存不足等)。

6. 相似函数,或关联函数

  • bind: 将套接字与一个本地地址(IP 地址和端口号)关联起来。
  • listen: 使套接字进入监听状态,准备接收来自客户端的连接请求(用于服务器)。
  • accept: 从监听套接字的连接队列中提取第一个未决连接,创建一个新的套接字用于与该客户端通信(用于服务器)。
  • connect: 主动向服务器发起连接请求(用于客户端)。
  • close: 关闭套接字文件描述符,释放相关资源。
  • read / write / send / recv: 通过套接字发送和接收数据。
  • getaddrinfo: 现代的、线程安全的地址解析函数,用于将主机名和服务名转换为套接字地址结构。

7. 示例代码

示例 1:创建不同类型的套接字

这个例子演示了如何创建几种常见的套接字。

#include <sys/socket.h> // socket
#include <stdio.h>      // perror, printf
#include <stdlib.h>     // exit

int main() {
    int sockfd;

    // 1. 创建一个 IPv4 的 TCP 流式套接字 (最常用)
    printf("Creating AF_INET, SOCK_STREAM socket...\n");
    sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (sockfd == -1) {
        perror("socket AF_INET SOCK_STREAM");
        // 不 exit,继续演示其他类型
    } else {
        printf("Success! Socket file descriptor: %d\n", sockfd);
        close(sockfd); // 创建后立即关闭,仅作演示
    }

    // 2. 创建一个 IPv4 的 UDP 数据报套接字
    printf("\nCreating AF_INET, SOCK_DGRAM socket...\n");
    sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    if (sockfd == -1) {
        perror("socket AF_INET SOCK_DGRAM");
    } else {
        printf("Success! Socket file descriptor: %d\n", sockfd);
        close(sockfd);
    }

    // 3. 创建一个 Unix 域流式套接字
    printf("\nCreating AF_UNIX, SOCK_STREAM socket...\n");
    sockfd = socket(AF_UNIX, SOCK_STREAM, 0);
    if (sockfd == -1) {
        perror("socket AF_UNIX SOCK_STREAM");
    } else {
        printf("Success! Socket file descriptor: %d\n", sockfd);
        close(sockfd);
    }

    // 4. 创建一个非阻塞的 TCP 套接字 (使用 SOCK_NONBLOCK 修饰符)
    printf("\nCreating non-blocking AF_INET, SOCK_STREAM socket...\n");
    sockfd = socket(AF_INET, SOCK_STREAM | SOCK_NONBLOCK, 0);
    if (sockfd == -1) {
        perror("socket AF_INET SOCK_STREAM | SOCK_NONBLOCK");
    } else {
        printf("Success! Non-blocking socket file descriptor: %d\n", sockfd);
        // 检查是否真的非阻塞 (可选)
        // int flags = fcntl(sockfd, F_GETFL, 0);
        // if (flags & O_NONBLOCK) printf("Confirmed: socket is non-blocking.\n");
        close(sockfd);
    }

    // 5. 尝试创建一个无效的套接字组合 (例如,原始套接字需要权限)
    printf("\nTrying to create a raw socket (may fail without root)...\n");
    sockfd = socket(AF_INET, SOCK_RAW, IPPROTO_ICMP);
    if (sockfd == -1) {
        perror("socket AF_INET SOCK_RAW IPPROTO_ICMP (expected to fail without root)");
    } else {
        printf("Success! Raw socket file descriptor: %d\n", sockfd);
        close(sockfd);
    }

    printf("\nSocket creation examples completed.\n");
    return 0;
}

代码解释:

  1. 演示了创建四种不同类型的套接字:
    • IPv4 TCP 流式套接字 (AF_INETSOCK_STREAM):这是网络编程中最常见的类型,用于可靠的、面向连接的通信(如 HTTP)。
    • IPv4 UDP 数据报套接字 (AF_INETSOCK_DGRAM):用于无连接的、不可靠但快速的通信(如 DNS 查询)。
    • Unix 域流式套接字 (AF_UNIXSOCK_STREAM):用于同一主机上进程间的高效通信。
    • 非阻塞 TCP 套接字 (AF_INETSOCK_STREAM | SOCK_NONBLOCK):使用 SOCK_NONBLOCK 修饰符创建,避免后续 I/O 操作阻塞。
  2. 每次调用 socket 后都检查返回值。
  3. 如果成功,打印返回的文件描述符,并立即调用 close 关闭它(因为这只是演示创建)。
  4. 最后尝试创建一个需要 root 权限的原始套接字 (SOCK_RAW),在普通用户权限下会失败。

示例 2:简单的 TCP 服务器套接字设置

这个例子演示了服务器端如何创建、绑定、监听一个 TCP 套接字。

#include <sys/socket.h> // socket, bind, listen
#include <netinet/in.h> // sockaddr_in
#include <arpa/inet.h>  // inet_addr (虽然此例未用,但常与网络编程相关)
#include <unistd.h>     // close
#include <stdio.h>      // perror, printf
#include <stdlib.h>     // exit
#include <string.h>     // memset

#define PORT 8080
#define BACKLOG 10 // 等待连接队列的最大长度

int main() {
    int server_fd;
    struct sockaddr_in address;
    int opt = 1; // 用于 setsockopt

    // 1. 创建套接字
    printf("Creating server socket...\n");
    server_fd = socket(AF_INET, SOCK_STREAM, 0);
    if (server_fd == -1) {
        perror("socket creation failed");
        exit(EXIT_FAILURE);
    }
    printf("Socket created successfully. File descriptor: %d\n", server_fd);

    // 2. (可选但推荐) 设置套接字选项
    // SO_REUSEADDR: 允许套接字绑定到处于 TIME_WAIT 状态的地址
    if (setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt))) {
        perror("setsockopt SO_REUSEADDR failed");
        close(server_fd);
        exit(EXIT_FAILURE);
    }
    printf("Socket option SO_REUSEADDR set.\n");

    // 3. 配置服务器地址结构
    memset(&address, 0, sizeof(address)); // 清零结构体
    address.sin_family = AF_INET;         // IPv4
    address.sin_addr.s_addr = INADDR_ANY; // 绑定到所有本地接口 (0.0.0.0)
    address.sin_port = htons(PORT);       // 端口号,从主机字节序转换为网络字节序

    // 4. 将套接字绑定到地址和端口
    printf("Binding socket to port %d...\n", PORT);
    if (bind(server_fd, (struct sockaddr *)&address, sizeof(address)) < 0) {
        perror("bind failed");
        close(server_fd);
        exit(EXIT_FAILURE);
    }
    printf("Socket bound successfully.\n");

    // 5. 让套接字进入监听状态
    printf("Putting socket into listening mode (backlog: %d)...\n", BACKLOG);
    if (listen(server_fd, BACKLOG) < 0) {
        perror("listen failed");
        close(server_fd);
        exit(EXIT_FAILURE);
    }
    printf("Server is now listening for incoming connections.\n");

    printf("\nServer setup complete. Waiting for connections...\n");
    printf("(Run a client to connect, e.g., 'telnet localhost %d' or 'nc localhost %d')\n", PORT, PORT);

    // --- 服务器已准备好,可以调用 accept() 来接受连接 ---
    // 这里为了演示 socket, bind, listen,暂时不实现 accept 循环

    // (在实际服务器中,这里会有一个循环调用 accept, fork/handle, close client_sock)

    // 按 Ctrl+C 退出程序
    pause(); // 永久挂起,直到收到信号

    // 6. 关闭套接字 (在实际程序中,这会在适当的地方调用)
    close(server_fd);
    printf("Server socket closed.\n");

    return 0;
}

代码解释:

  1. 调用 socket(AF_INET, SOCK_STREAM, 0) 创建一个 IPv4 TCP 套接字。
  2. (重要) 调用 setsockopt 设置 SO_REUSEADDR 选项。这允许服务器在重启时立即绑定到同一个地址,即使之前的连接可能处于 TIME_WAIT 状态。这是一个很好的实践。
  3. 初始化 sockaddr_in 结构体 address 来指定服务器的地址和端口:
    • sin_family = AF_INET:指定 IPv4。
    • sin_addr.s_addr = INADDR_ANY:绑定到所有可用的网络接口(服务器可能有多个网卡)。如果只想绑定到特定 IP,可以使用 inet_addr("192.168.1.100") 之类的函数。
    • sin_port = htons(PORT):设置端口号。重要:使用 htons() (host to network short) 将主机字节序的端口号转换为网络字节序。网络协议要求使用大端字节序。
  4. 调用 bind(server_fd, ...) 将套接字与指定的地址和端口绑定。
  5. 调用 listen(server_fd, BACKLOG) 使套接字进入监听模式。BACKLOG 参数指定了内核为此套接字维护的未完成连接队列的最大长度。
  6. 此时,服务器已准备好接收客户端连接。后续需要在一个循环中调用 accept() 来处理连接。
  7. 为了演示,程序调用 pause() 挂起,等待用户按 Ctrl+C 退出。
  8. 程序退出前关闭服务器套接字。

编译和测试:

gcc -o tcp_server tcp_server.c
./tcp_server
# 在另一个终端:
# telnet localhost 8080
# 或者
# nc localhost 8080

示例 3:简单的 TCP 客户端套接字设置

这个例子演示了客户端如何创建套接字并连接到服务器。

#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h> // inet_addr, inet_ntoa
#include <unistd.h>    // close, read, write
#include <stdio.h>     // perror, printf
#include <stdlib.h>    // exit
#include <string.h>    // strlen, memset

#define PORT 8080
#define SERVER_IP "127.0.0.1" // 本地回环地址

int main() {
    int sock = 0;
    struct sockaddr_in serv_addr;
    char *hello = "Hello from client";
    char buffer[1024] = {0};

    // 1. 创建套接字
    printf("Creating client socket...\n");
    if ((sock = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
        perror("socket creation error");
        exit(EXIT_FAILURE);
    }
    printf("Client socket created successfully. File descriptor: %d\n", sock);

    // 2. 配置服务器地址结构
    memset(&serv_addr, 0, sizeof(serv_addr));
    serv_addr.sin_family = AF_INET;
    serv_addr.sin_port = htons(PORT);

    // 将服务器 IP 地址从文本转换为二进制
    // inet_addr 已过时,推荐使用 inet_pton
    // if (inet_addr(SERVER_IP) == INADDR_NONE) { ... handle error ... }
    if (inet_pton(AF_INET, SERVER_IP, &serv_addr.sin_addr) <= 0) {
        perror("inet_pton error or invalid address");
        close(sock);
        exit(EXIT_FAILURE);
    }

    // 3. 连接到服务器
    printf("Connecting to server %s:%d...\n", SERVER_IP, PORT);
    if (connect(sock, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) < 0) {
        perror("connection failed");
        close(sock);
        exit(EXIT_FAILURE);
    }
    printf("Connected to server successfully.\n");

    // 4. 发送数据到服务器
    printf("Sending message to server: %s\n", hello);
    if (send(sock, hello, strlen(hello), 0) != (int)strlen(hello)) {
         perror("send failed");
         close(sock);
         exit(EXIT_FAILURE);
    }
    printf("Message sent.\n");

    // 5. 读取服务器的响应
    printf("Reading response from server...\n");
    int valread = read(sock, buffer, 1024);
    if (valread > 0) {
        printf("Received from server: %s\n", buffer);
    } else if (valread == 0) {
        printf("Server closed the connection.\n");
    } else {
        perror("read failed");
    }

    // 6. 关闭套接字
    close(sock);
    printf("Client socket closed.\n");

    return 0;
}

代码解释:

  1. 调用 socket(AF_INET, SOCK_STREAM, 0) 创建一个 IPv4 TCP 套接字。
  2. 初始化 sockaddr_in 结构体 serv_addr 来指定服务器的地址和端口。
    • sin_family = AF_INET
    • sin_port = htons(PORT):服务器端口。
    • sin_addr:服务器 IP 地址。使用 inet_pton() 将点分十进制字符串 "127.0.0.1" 转换为网络二进制格式。inet_addr() 是旧函数,不推荐。
  3. 调用 connect(sock, ...) 主动向服务器发起连接请求。这个调用会阻塞,直到连接建立或失败。
  4. 连接成功后,使用 send() (或 write()) 向服务器发送数据。
  5. 使用 read() (或 recv()) 从服务器读取响应数据。
  6. 通信结束后,调用 close() 关闭套接字。

重要提示与注意事项:

  1. 字节序: 网络协议规定使用大端字节序(Big-Endian)。主机字节序可能是大端或小端。使用 htons (host to network short), htonl (host to network long), ntohsntohl 进行转换。
  2. 错误处理: 始终检查 socket 及后续网络函数的返回值。
  3. 资源管理: 使用完套接字后,务必调用 close() 关闭它,以释放文件描述符和内核资源。
  4. 阻塞与非阻塞: 默认情况下,套接字是阻塞的。connectreadwrite 等操作可能会无限期挂起。可以使用 SOCK_NONBLOCK 创建非阻塞套接字,或用 fcntl 修改现有套接字的标志。
  5. bind 对于客户端?: 客户端通常不需要显式调用 bind。操作系统会自动为客户端套接字分配一个临时的端口号(ephemeral port)。
  6. getaddrinfo: 对于需要处理 IPv4/IPv6 透明性或域名解析的现代程序,推荐使用 getaddrinfo() 来获取地址信息,而不是手动填充 sockaddr_in

总结:

socket 函数是网络编程的起点,它创建了通信的端点。理解其参数(域、类型、协议)对于选择正确的通信方式至关重要。它是构建客户端和服务器应用程序的基础。

此条目发表在linux文章分类目录,贴了标签。将固定链接加入收藏夹。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注