Administrator
Administrator
发布于 2024-12-03 / 6 阅读
0
0

socket 函数详解

socket 函数详解

socket 函数是C语言网络编程中的核心函数之一,用于创建一个套接字(Socket),这是进行网络通信的基础。理解 socket 函数的使用方法和参数,对于开发网络应用程序(如客户端、服务器)至关重要。本文将详细介绍 socket 函数的定义、参数、返回值、常见用法及示例代码。


目录

  1. socket 函数简介
  2. socket 函数的原型
  3. socket 函数的参数详解
  4. socket 函数的返回值
  5. socket 函数的常见用法
  6. socket 函数的错误处理
  7. 完整示例:创建TCP客户端套接字
  8. 常见问题与注意事项
  9. 总结

1. socket 函数简介

socket 函数用于创建一个套接字,这是进行网络通信的起点。套接字提供了一个通信端点,使得两个网络设备之间能够发送和接收数据。不同类型的套接字支持不同的通信协议和模式,适用于不同的应用场景。


2. socket 函数的原型

#include <sys/types.h>
#include <sys/socket.h>

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

参数解释

  • domain:协议族,用于指定通信协议的地址族。
  • type:套接字类型,定义了套接字的通信方式。
  • protocol:具体协议,通常可以设为0以自动选择。

返回值

  • 成功:返回一个非负的套接字描述符(文件描述符),用于后续的网络操作。
  • 失败:返回 -1,并设置 errno 以指示错误原因。

3. socket 函数的参数详解

3.1 协议族(Address Family)

协议族决定了套接字将使用哪种协议进行通信,常见的协议族包括:

  • AF_INET:IPv4地址族,适用于IPv4网络通信。
  • AF_INET6:IPv6地址族,适用于IPv6网络通信。
  • AF_UNIX(或 AF_LOCAL):本地套接字,用于同一主机内的进程间通信(IPC)。
  • AF_PACKET:低级别的数据链路层套接字,用于访问网络层以下的数据包(主要用于Linux)。

示例

int sockfd = socket(AF_INET, SOCK_STREAM, 0);

3.2 套接字类型(Socket Type)

套接字类型决定了通信的模式和特性,常见的套接字类型包括:

  • SOCK_STREAM:流套接字,提供面向连接、可靠的双向字节流通信,通常使用TCP协议。
  • SOCK_DGRAM:数据报套接字,提供无连接、不可靠的消息传输,通常使用UDP协议。
  • SOCK_RAW:原始套接字,允许直接访问网络层协议(如IP),用于高级网络应用和工具。
  • SOCK_SEQPACKET:顺序数据包套接字,提供面向连接、可靠的消息传输,保持消息边界。
  • SOCK_RDM:可靠数据报套接字,提供可靠但无序的数据传输。

示例

int sockfd = socket(AF_INET, SOCK_DGRAM, 0);

3.3 协议(Protocol)

协议参数用于指定具体的通信协议,通常可以设置为0,让系统自动选择合适的协议。常见的协议包括:

  • IPPROTO_TCP:传输控制协议(TCP)。
  • IPPROTO_UDP:用户数据报协议(UDP)。
  • IPPROTO_ICMP:互联网控制消息协议(ICMP)。
  • IPPROTO_RAW:原始IP协议,用于原始套接字。

示例

int sockfd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);

4. socket 函数的返回值

  • 成功:返回一个整数,表示创建的套接字的文件描述符,可以用于后续的网络操作(如 bindconnectsendrecv 等)。

    int sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (sockfd == -1) {
        perror("socket creation failed");
        exit(EXIT_FAILURE);
    }
    
  • 失败:返回 -1,并设置 errno 以指示错误原因。常见错误包括:

    • EACCES:权限不足,通常尝试创建原始套接字但缺少超级用户权限。
    • EAFNOSUPPORT:不支持指定的协议族。
    • EINVAL:无效的协议参数。
    • EMFILE:进程已打开的文件描述符达到上限。
    • ENFILE:系统打开的文件描述符达到上限。
    • ENOBUFS / ENOMEM:系统内存不足。
    • EPROTONOSUPPORT:不支持指定的协议。

5. socket 函数的常见用法

5.1 创建TCP套接字

用于建立可靠的、面向连接的通信(通常是客户端和服务器之间的通信)。

步骤

  1. 创建套接字:使用 AF_INETSOCK_STREAM
  2. 设置服务器地址
  3. 连接到服务器(客户端)或 绑定并监听(服务器)。
  4. 进行数据传输
  5. 关闭套接字

示例

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>

int main() {
    // 创建TCP套接字
    int sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (sockfd == -1) {
        perror("socket creation failed");
        exit(EXIT_FAILURE);
    }

    // 服务器地址
    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) {
        perror("invalid address/ Address not supported");
        close(sockfd);
        exit(EXIT_FAILURE);
    }

    // 连接到服务器
    if (connect(sockfd, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0) {
        perror("connection failed");
        close(sockfd);
        exit(EXIT_FAILURE);
    }

    // 发送数据
    char *message = "Hello, Server!";
    send(sockfd, message, strlen(message), 0);
    printf("Message sent to server: %s\n", message);

    // 接收数据
    char buffer[1024] = {0};
    int bytes_received = recv(sockfd, buffer, sizeof(buffer), 0);
    if (bytes_received > 0) {
        printf("Message from server: %s\n", buffer);
    }

    // 关闭套接字
    close(sockfd);
    return 0;
}

5.2 创建UDP套接字

用于无连接、不可靠的消息传输,适用于实时应用和需要广播的场景。

步骤

  1. 创建套接字:使用 AF_INETSOCK_DGRAM
  2. 设置目标地址
  3. 发送和接收数据
  4. 关闭套接字

示例

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>

int main() {
    // 创建UDP套接字
    int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    if (sockfd == -1) {
        perror("socket creation failed");
        exit(EXIT_FAILURE);
    }

    // 目标地址
    struct sockaddr_in dest_addr;
    memset(&dest_addr, 0, sizeof(dest_addr)); // 初始化
    dest_addr.sin_family = AF_INET;
    dest_addr.sin_port = htons(8080); // 目标端口

    // 将IP地址从文本转换为二进制形式
    if (inet_pton(AF_INET, "127.0.0.1", &dest_addr.sin_addr) <= 0) {
        perror("invalid address/ Address not supported");
        close(sockfd);
        exit(EXIT_FAILURE);
    }

    // 发送数据
    char *message = "Hello, UDP Server!";
    sendto(sockfd, message, strlen(message), 0, (struct sockaddr *)&dest_addr, sizeof(dest_addr));
    printf("Message sent to server: %s\n", message);

    // 接收数据
    char buffer[1024] = {0};
    struct sockaddr_in src_addr;
    socklen_t addr_len = sizeof(src_addr);
    int bytes_received = recvfrom(sockfd, buffer, sizeof(buffer), 0, (struct sockaddr *)&src_addr, &addr_len);
    if (bytes_received > 0) {
        printf("Message from server: %s\n", buffer);
    }

    // 关闭套接字
    close(sockfd);
    return 0;
}

5.3 创建原始套接字

用于直接访问网络层协议(如IP),适用于开发网络工具、监控和安全应用。需要超级用户权限。

注意:原始套接字的使用涉及复杂的网络协议知识,并且存在安全风险,通常仅限于高级网络编程和系统管理任务。

示例:发送ICMP回显请求(类似于ping命令)。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/ip_icmp.h>
#include <arpa/inet.h>
#include <errno.h>

// 计算校验和
unsigned short checksum(void *b, int len) {    
    unsigned short *buf = b;
    unsigned int sum = 0;
    unsigned short result;
    
    for(sum = 0; len > 1; len -= 2)
        sum += *buf++;
    if(len == 1)
        sum += *(unsigned char*)buf;
    sum = (sum >> 16) + (sum & 0xFFFF);
    sum += (sum >> 16);
    result = ~sum;
    return result;
}

int main(int argc, char *argv[]) {
    if(argc != 2) {
        fprintf(stderr, "Usage: %s <IP Address>\n", argv[0]);
        exit(EXIT_FAILURE);
    }

    // 创建原始套接字
    int sockfd = socket(AF_INET, SOCK_RAW, IPPROTO_ICMP);
    if(sockfd < 0) {
        perror("socket creation failed");
        exit(EXIT_FAILURE);
    }

    // 目标地址
    struct sockaddr_in dest_addr;
    memset(&dest_addr, 0, sizeof(dest_addr));
    dest_addr.sin_family = AF_INET;
    if(inet_pton(AF_INET, argv[1], &dest_addr.sin_addr) <= 0) {
        fprintf(stderr, "Invalid IP address: %s\n", argv[1]);
        close(sockfd);
        exit(EXIT_FAILURE);
    }

    // 构造ICMP回显请求
    char packet[64];
    memset(packet, 0, sizeof(packet));

    struct icmphdr *icmp = (struct icmphdr *)packet;
    icmp->type = ICMP_ECHO;
    icmp->code = 0;
    icmp->un.echo.id = getpid();
    icmp->un.echo.sequence = 1;
    // 填充数据部分
    strcpy(packet + sizeof(struct icmphdr), "Hello, ICMP!");

    // 计算校验和
    icmp->checksum = checksum((unsigned short *)packet, sizeof(packet));

    // 发送ICMP回显请求
    if(sendto(sockfd, packet, sizeof(packet), 0, (struct sockaddr *)&dest_addr, sizeof(dest_addr)) <= 0) {
        perror("sendto failed");
        close(sockfd);
        exit(EXIT_FAILURE);
    }
    printf("ICMP Echo Request sent to %s\n", argv[1]);

    // 接收ICMP回显响应
    char recv_buffer[1024];
    struct sockaddr_in recv_addr;
    socklen_t addr_len = sizeof(recv_addr);
    ssize_t bytes_received = recvfrom(sockfd, recv_buffer, sizeof(recv_buffer), 0, (struct sockaddr *)&recv_addr, &addr_len);
    if(bytes_received < 0) {
        perror("recvfrom failed");
        close(sockfd);
        exit(EXIT_FAILURE);
    }

    // 解析IP头
    struct iphdr *ip = (struct iphdr *)recv_buffer;
    int ip_header_len = ip->ihl * 4;

    // 解析ICMP头
    struct icmphdr *recv_icmp = (struct icmphdr *)(recv_buffer + ip_header_len);

    if(recv_icmp->type == ICMP_ECHOREPLY) {
        printf("Received ICMP Echo Reply from %s\n", inet_ntoa(*(struct in_addr*)&ip->saddr));
    } else {
        printf("Received ICMP type %d code %d\n", recv_icmp->type, recv_icmp->code);
    }

    close(sockfd);
    return 0;
}

编译与运行

由于创建原始套接字需要超级用户权限,编译并运行时需要使用 sudo 或以root身份运行。

gcc -o raw_ping raw_ping.c
sudo ./raw_ping 8.8.8.8

示例输出

ICMP Echo Request sent to 8.8.8.8
Received ICMP Echo Reply from 8.8.8.8

6. socket 函数的错误处理

在使用 socket 函数时,必须对其返回值进行检查,以确保套接字的创建是否成功。常见的错误处理方法包括:

检查返回值

int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd == -1) {
    perror("socket creation failed");
    exit(EXIT_FAILURE);
}

处理不同错误码

根据 errno 的值,可以识别并处理不同的错误原因。例如:

  • EACCES:权限不足,通常尝试创建原始套接字但缺少超级用户权限。
  • EAFNOSUPPORT:不支持指定的协议族。
  • EPROTONOSUPPORT:不支持指定的协议。
  • EMFILE:进程打开的文件描述符达到上限。
  • ENOBUFS / ENOMEM:系统内存不足。

示例

#include <errno.h>

// 创建套接字
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd == -1) {
    switch (errno) {
        case EACCES:
            fprintf(stderr, "权限不足,无法创建套接字\n");
            break;
        case EAFNOSUPPORT:
            fprintf(stderr, "不支持指定的协议族\n");
            break;
        case EPROTONOSUPPORT:
            fprintf(stderr, "不支持指定的协议\n");
            break;
        case EMFILE:
            fprintf(stderr, "进程打开的文件描述符达到上限\n");
            break;
        case ENOBUFS:
        case ENOMEM:
            fprintf(stderr, "系统内存不足,无法创建套接字\n");
            break;
        default:
            perror("socket creation failed");
    }
    exit(EXIT_FAILURE);
}

7. 完整示例:创建TCP客户端套接字

以下是一个完整的TCP客户端示例,展示了如何使用 socket 函数创建套接字,并与服务器建立连接、发送和接收数据。

TCP客户端示例代码

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>

#define PORT 8080
#define MAXLINE 1024

int main(int argc, char *argv[]) {
    int sockfd;
    struct sockaddr_in servaddr;
    char buffer[MAXLINE];
    char *hello = "Hello from client";

    // 创建套接字
    sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (sockfd == -1) {
        perror("socket creation failed");
        exit(EXIT_FAILURE);
    }
    printf("Socket successfully created.\n");

    memset(&servaddr, 0, sizeof(servaddr));

    // 服务器地址配置
    servaddr.sin_family = AF_INET;
    servaddr.sin_port = htons(PORT);

    // 将IP地址从文本转换为二进制形式
    if(inet_pton(AF_INET, "127.0.0.1", &servaddr.sin_addr) <= 0) {
        fprintf(stderr, "Invalid address/ Address not supported\n");
        close(sockfd);
        exit(EXIT_FAILURE);
    }

    // 连接到服务器
    if (connect(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) != 0) {
        perror("connection with the server failed");
        close(sockfd);
        exit(EXIT_FAILURE);
    }
    printf("Connected to the server.\n");

    // 发送数据
    send(sockfd, hello, strlen(hello), 0);
    printf("Hello message sent.\n");

    // 接收数据
    int n = recv(sockfd, buffer, sizeof(buffer)-1, 0);
    if (n < 0) {
        perror("recv failed");
    } else {
        buffer[n] = '\0';
        printf("Server : %s\n", buffer);
    }

    // 关闭套接字
    close(sockfd);
    return 0;
}

TCP服务器示例代码

为了测试上述客户端,需要一个简单的TCP服务器。以下是一个基本的TCP服务器示例:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>

#define PORT 8080
#define MAXLINE 1024

int main() {
    int sockfd, connfd;
    struct sockaddr_in servaddr, cliaddr;
    socklen_t len;
    char buffer[MAXLINE];
    char *reply = "Hello from server";

    // 创建套接字
    sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (sockfd == -1) {
        perror("socket creation failed");
        exit(EXIT_FAILURE);
    }
    printf("Socket successfully created.\n");

    memset(&servaddr, 0, sizeof(servaddr));

    // 服务器地址配置
    servaddr.sin_family = AF_INET;
    servaddr.sin_addr.s_addr = INADDR_ANY; // 绑定到所有可用接口
    servaddr.sin_port = htons(PORT);

    // 绑定套接字
    if (bind(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) != 0) {
        perror("bind failed");
        close(sockfd);
        exit(EXIT_FAILURE);
    }
    printf("Socket successfully binded.\n");

    // 监听连接
    if (listen(sockfd, 5) != 0) {
        perror("listen failed");
        close(sockfd);
        exit(EXIT_FAILURE);
    }
    printf("Server listening on port %d.\n", PORT);

    len = sizeof(cliaddr);

    // 接受连接
    connfd = accept(sockfd, (struct sockaddr *)&cliaddr, &len);
    if (connfd < 0) {
        perror("server accept failed");
        close(sockfd);
        exit(EXIT_FAILURE);
    }
    printf("Client connected.\n");

    // 接收数据
    int n = recv(connfd, buffer, sizeof(buffer)-1, 0);
    if (n < 0) {
        perror("recv failed");
    } else {
        buffer[n] = '\0';
        printf("Client : %s\n", buffer);
    }

    // 发送回复
    send(connfd, reply, strlen(reply), 0);
    printf("Reply message sent.\n");

    // 关闭连接和套接字
    close(connfd);
    close(sockfd);
    return 0;
}

编译与运行

  1. 编译服务器和客户端

    gcc -o tcp_server tcp_server.c
    gcc -o tcp_client tcp_client.c
    
  2. 运行服务器

    ./tcp_server
    
  3. 运行客户端(在另一个终端):

    ./tcp_client
    

示例输出

服务器端

Socket successfully created.
Socket successfully binded.
Server listening on port 8080.
Client connected.
Client : Hello from client
Reply message sent.

客户端

Socket successfully created.
Connected to the server.
Hello message sent.
Server : Hello from server

8. 常见问题与注意事项

8.1 权限问题

  • 原始套接字(SOCK_RAW:大多数操作系统要求超级用户权限才能创建原始套接字。尝试以普通用户运行可能会导致 socket 函数失败,并返回 EPERM(操作不允许)或 EACCES(权限不足)。

8.2 协议选择

  • 自动选择协议:当第三个参数 protocol 设置为0时,系统会根据套接字类型自动选择合适的协议。

    int sockfd = socket(AF_INET, SOCK_STREAM, 0); // 自动选择TCP
    
  • 明确选择协议:可以通过指定 protocol 参数明确选择所需的协议。

    int sockfd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
    

8.3 套接字关闭

  • 及时关闭:使用完套接字后,应及时调用 close(sockfd) 关闭套接字,释放系统资源。

    close(sockfd);
    

8.4 非阻塞模式

  • 设置非阻塞套接字:可以使用 fcntl 函数将套接字设置为非阻塞模式,适用于需要并发处理的应用。

    #include <fcntl.h>
    
    int flags = fcntl(sockfd, F_GETFL, 0);
    fcntl(sockfd, F_SETFL, flags | O_NONBLOCK);
    

8.5 多套接字管理

  • 使用 selectpollepoll:在处理多个套接字时,可以使用这些I/O复用机制来同时监视多个套接字的事件。

    示例:使用 select 监视多个套接字。

    #include <sys/select.h>
    
    // 初始化fd_set
    fd_set readfds;
    FD_ZERO(&readfds);
    FD_SET(sockfd1, &readfds);
    FD_SET(sockfd2, &readfds);
    
    // 设置最大文件描述符
    int maxfd = (sockfd1 > sockfd2) ? sockfd1 : sockfd2;
    
    // 调用select
    int activity = select(maxfd + 1, &readfds, NULL, NULL, NULL);
    
    if (FD_ISSET(sockfd1, &readfds)) {
        // 处理sockfd1的读事件
    }
    
    if (FD_ISSET(sockfd2, &readfds)) {
        // 处理sockfd2的读事件
    }
    

8.6 地址和端口转换

  • 网络字节序:网络协议使用大端字节序,使用 htons(主机到网络短整数)和 htonl(主机到网络长整数)将端口号和IP地址转换为网络字节序,使用 ntohsntohl 进行逆转换。

    servaddr.sin_port = htons(8080); // 转换端口号
    servaddr.sin_addr.s_addr = inet_addr("127.0.0.1"); // 转换IP地址
    

8.7 处理部分发送和接收

  • sendrecv:这些函数可能不会一次性发送或接收所有数据,尤其是在大数据量传输时。需要在应用层实现循环,确保所有数据都被正确发送和接收。

    ssize_t total_sent = 0;
    ssize_t message_len = strlen(message);
    
    while (total_sent < message_len) {
        ssize_t sent = send(sockfd, message + total_sent, message_len - total_sent, 0);
        if (sent == -1) {
            perror("send failed");
            break;
        }
        total_sent += sent;
    }
    

9. 总结

socket 函数是C语言网络编程中不可或缺的一部分,提供了创建套接字以实现不同类型的网络通信的基础。通过正确理解和使用 socket 函数的参数和返回值,开发者可以构建各种网络应用程序,包括客户端、服务器、实时通信工具以及网络监控和安全应用。

关键要点

  • 协议族(domain:决定套接字的地址族,如 AF_INET(IPv4)、AF_INET6(IPv6)。
  • 套接字类型(type:决定通信模式,如 SOCK_STREAM(TCP)、SOCK_DGRAM(UDP)、SOCK_RAW(原始套接字)。
  • 协议(protocol:指定具体的通信协议,通常设为0自动选择,或明确指定如 IPPROTO_TCPIPPROTO_UDP
  • 返回值:成功返回套接字描述符,失败返回 -1 并设置 errno
  • 错误处理:检查 socket 函数的返回值,并根据 errno 处理不同的错误情况。
  • 权限要求:创建原始套接字通常需要超级用户权限。
  • 资源管理:使用完套接字后,及时关闭以释放系统资源。

通过掌握这些知识,您可以有效地使用 socket 函数进行网络编程,开发出功能丰富且高效的网络应用程序。如果您有更多关于 socket 函数或网络编程的疑问,欢迎继续提问!


评论