C语言socket编程如何实现网络通信?

99ANYc3cd6
预计阅读时长 31 分钟
位置: 首页 C语言 正文

目录

  1. 什么是 Socket?
  2. Socket 编程基本流程
  3. 核心 API 函数详解
  4. 一个完整的示例:简单的 Echo 服务器与客户端
  5. 关键注意事项
  6. 总结与进阶

什么是 Socket?

你可以把 Socket(套接字) 想象成一个通信的“端点”,它是一个编程接口,允许程序发送或接收数据,就像一个电话插座,你把电话线(网络连接)插进去,就可以进行通话(数据交换)。

c语言 socket 编程
(图片来源网络,侵删)

在 C 语言中,Socket 不是一个数据类型,而是一个文件描述符,它是一个整数,操作系统用它来标识一个网络连接,我们可以像读写文件一样对 Socket 进行读写操作。

Socket 主要分为两种类型:

  • 流式套接字:使用 TCP (Transmission Control Protocol) 协议,它提供面向连接的、可靠的、有序的、基于字节流的数据传输服务,数据不会丢失、重复或乱序,打电话的比喻很形象:你必须先拨号建立连接,然后才能通话,通话结束后挂断。
  • 数据报套接字:使用 UDP (User Datagram Protocol) 协议,它提供无连接的、不可靠的、基于数据报的数据传输服务,数据可能会丢失、重复或乱序,寄明信片的比喻很形象:你写好地址直接投递,不需要提前联系对方,也无法保证明信片能按时、按顺序到达。

本指南将重点介绍更常用的 TCP Socket 编程


Socket 编程基本流程

TCP 编程分为服务器端和客户端,两者的流程是不同的。

c语言 socket 编程
(图片来源网络,侵删)

服务器端 流程

  1. 创建套接字:使用 socket() 函数创建一个通信端点。
  2. 绑定地址和端口:使用 bind() 函数将套接字与一个特定的 IP 地址和端口号绑定,这样客户端才知道连接到哪里。
  3. 监听连接:使用 listen() 函数将套接字设置为被动模式,准备接受客户端的连接请求。
  4. 接受连接:使用 accept() 函数阻塞等待客户端的连接请求,当有客户端连接时,accept() 返回一个新的套接字,专门用于与这个客户端通信。
  5. 收发数据:使用 send()recv() (或 read()write()) 函数通过新的套接字与客户端进行数据交换。
  6. 关闭套接字:通信结束后,使用 close() 关闭用于通信的套接字,最后再关闭监听套接字。

客户端 流程

  1. 创建套接字:同样使用 socket() 函数创建一个通信端点。
  2. 连接服务器:使用 connect() 函数主动向服务器的 IP 地址和端口号发起连接请求。
  3. 收发数据:连接成功后,使用 send()recv() (或 read()write()) 函数与服务器进行数据交换。
  4. 关闭套接字:通信结束后,使用 close() 关闭套接字。

核心 API 函数详解

在使用这些函数前,需要包含必要的头文件:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h> // 用于 read, write, close
#include <sys/socket.h> // 核心 socket 函数
#include <netinet/in.h> // sockaddr_in 结构体
#include <arpa/inet.h> // inet_addr 函数

socket()

int socket(int domain, int type, int protocol);
  • 功能: 创建一个套接字。
  • 参数:
    • domain: 地址族,常用 AF_INET (IPv4) 或 AF_INET6 (IPv6)。
    • type: 套接字类型,常用 SOCK_STREAM (TCP) 或 SOCK_DGRAM (UDP)。
    • protocol: 协议,通常设为 0,系统会自动根据 domaintype 选择。
  • 返回值: 成功返回套接字描述符(一个整数),失败返回 -1

bind()

int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
  • 功能: 将套接字与一个 IP 地址和端口号绑定。
  • 参数:
    • sockfd: socket() 返回的套接字描述符。
    • addr: 指向 sockaddr 结构体的指针,实际使用时,我们通常用 sockaddr_in 结构体并强制转换。
    • addrlen: addr 结构体的长度。
  • 返回值: 成功返回 0,失败返回 -1

listen()

int listen(int sockfd, int backlog);
  • 功能: 将套接字设置为被动监听模式,等待客户端连接。
  • 参数:
    • sockfd: 已经绑定地址的套接字描述符。
    • backlog: 请求队列的最大长度,表示最多有多少个连接可以等待被 accept()
  • 返回值: 成功返回 0,失败返回 -1

accept()

int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
  • 功能: 从监听队列中接受一个连接请求,这是一个阻塞函数,直到有客户端连接为止。
  • 参数:
    • sockfd: 处于监听状态的套接字描述符。
    • addr: 用于保存客户端的地址信息(可选,可设为 NULL)。
    • addrlen: 指向 addr 结构体长度的指针(可选,可设为 NULL)。
  • 返回值: 成功返回一个新的套接字描述符(用于与客户端通信),失败返回 -1

connect()

int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
  • 功能: 客户端主动向服务器发起连接请求。
  • 参数:
    • sockfd: 客户端创建的套接字描述符。
    • addr: 指向服务器地址结构体的指针。
    • addrlen: 服务器地址结构体的长度。
  • 返回值: 成功返回 0,失败返回 -1

send() / recv()

ssize_t send(int sockfd, const void *buf, size_t len, int flags);
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
  • 功能: 在已连接的套接字上发送和接收数据。
  • 参数:
    • sockfd: 已连接的套接字描述符。
    • buf: 发送/接收数据的缓冲区。
    • len: 要发送/接收的数据长度。
    • flags: 通常设为 0
  • 返回值: 成功返回实际发送/接收的字节数(可能小于 len),连接断开时返回 0,失败返回 -1

close()

int close(int fd);
  • 功能: 关闭一个套接字(或文件)描述符。
  • 返回值: 成功返回 0,失败返回 -1

一个完整的示例:简单的 Echo 服务器与客户端

这个例子中,客户端发送一条消息,服务器收到后将原消息返回给客户端。

sockaddr_in 结构体

这是 sockaddr 的 IPv4 版本,我们在编程中主要使用它:

struct sockaddr_in {
    short            sin_family;   // 地址族,必须为 AF_INET
    unsigned short   sin_port;     // 端口号,需要用 htons() 转换
    struct in_addr  sin_addr;     // IP 地址,需要用 inet_addr() 或 inet_pton() 转换
    char             sin_zero[8];  // 填充,不用管
};

重要: sin_portsin_addr 的值在赋值前必须进行网络字节序转换。

  • htons(): Host to Network Short (16位)
  • htonl(): Host to Network Long (32位)
  • inet_addr(): 将点分十进制的 IP 字符串转换为网络字节序的 in_addr 结构。

服务器端代码 (server.c)

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#define PORT 8080
#define BUFFER_SIZE 1024
int main() {
    int server_fd, new_socket;
    struct sockaddr_in address;
    int opt = 1;
    int addrlen = sizeof(address);
    char buffer[BUFFER_SIZE] = {0};
    // 1. 创建套接字
    // AF_INET: IPv4, SOCK_STREAM: TCP, 0: 自动选择协议
    if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == 0) {
        perror("socket failed");
        exit(EXIT_FAILURE);
    }
    // 设置套接字选项,允许地址重用(避免服务器重启时端口被占用)
    if (setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &opt, sizeof(opt))) {
        perror("setsockopt");
        exit(EXIT_FAILURE);
    }
    address.sin_family = AF_INET; // IPv4
    address.sin_addr.s_addr = INADDR_ANY; // 监听所有可用的网络接口
    address.sin_port = htons(PORT); // 将端口号转换为网络字节序
    // 2. 绑定地址和端口
    if (bind(server_fd, (struct sockaddr *)&address, sizeof(address)) < 0) {
        perror("bind failed");
        exit(EXIT_FAILURE);
    }
    // 3. 监听连接
    if (listen(server_fd, 3) < 0) {
        perror("listen");
        exit(EXIT_FAILURE);
    }
    printf("Server listening on port %d...\n", PORT);
    // 4. 接受连接
    if ((new_socket = accept(server_fd, (struct sockaddr *)&address, (socklen_t*)&addrlen)) < 0) {
        perror("accept");
        exit(EXIT_FAILURE);
    }
    printf("Client connected: %s:%d\n", inet_ntoa(address.sin_addr), ntohs(address.sin_port));
    // 5. 收发数据
    int valread;
    while ((valread = read(new_socket, buffer, BUFFER_SIZE)) > 0) {
        printf("Received: %s", buffer);
        send(new_socket, buffer, valread, 0); // 将收到的数据回显给客户端
        memset(buffer, 0, BUFFER_SIZE); // 清空缓冲区
    }
    if (valread == 0) {
        printf("Client disconnected\n");
    } else if (valread < 0) {
        perror("read");
    }
    // 6. 关闭套接字
    close(new_socket);
    close(server_fd);
    return 0;
}

客户端代码 (client.c)

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#define PORT 8080
#define BUFFER_SIZE 1024
int main() {
    int sock = 0;
    struct sockaddr_in serv_addr;
    char *hello = "Hello from client";
    char buffer[BUFFER_SIZE] = {0};
    // 1. 创建套接字
    if ((sock = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
        printf("\n Socket creation error \n");
        return -1;
    }
    serv_addr.sin_family = AF_INET;
    serv_addr.sin_port = htons(PORT);
    // 将 IPv4 地址从文本转换为二进制形式
    if (inet_pton(AF_INET, "127.0.0.1", &serv_addr.sin_addr) <= 0) {
        printf("\nInvalid address/ Address not supported \n");
        return -1;
    }
    // 2. 连接服务器
    if (connect(sock, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) < 0) {
        printf("\nConnection Failed \n");
        return -1;
    }
    printf("Connected to server.\n");
    // 3. 发送数据
    send(sock, hello, strlen(hello), 0);
    printf("Hello message sent\n");
    // 4. 接收数据
    int valread = read(sock, buffer, BUFFER_SIZE);
    printf("Server echoed: %s\n", buffer);
    // 5. 关闭套接字
    close(sock);
    return 0;
}

关键注意事项

  1. 错误处理: 所有的 Socket API 调用都可能失败,必须检查它们的返回值,失败时,通常使用 perror() 打印错误信息,并用 exit() 退出程序。
  2. 字节序: 网络字节序是大端序,在设置端口号和 IP 地址时,必须使用 htons()htonl()inet_addr() 等函数进行转换,以确保在不同架构的机器上都能正确通信。
  3. 地址重用: 服务器端调用 setsockopt() 设置 SO_REUSEADDR 是一个好习惯,它可以让你在服务器快速重启时,不必等待 TIME_WAIT 状态结束就能重新绑定端口。
  4. 阻塞与非阻塞: accept()recv() 等函数默认是阻塞的,即如果没有数据到达,程序会一直等待,对于高性能服务器,需要使用 select(), poll(), 或 epoll (Linux) 等I/O多路复用技术来实现非阻塞I/O。
  5. 跨平台性: 本指南中的代码主要基于 Linux/Unix 系统(使用了 unistd.h),在 Windows 上,需要包含 <winsock2.h> 并在使用前调用 WSAStartup() 初始化,使用后调用 WSACleanup() 清理。

总结与进阶

  • 你已经了解了 C 语言 TCP Socket 编程的基本流程和核心 API,并成功实现了一个简单的 Echo 服务器和客户端,这是网络编程的“Hello, World”。
  • 进阶方向:
    1. 并发服务器: 上述服务器一次只能处理一个客户端,要同时处理多个客户端,你需要使用多线程 (pthread) 或多进程 (fork) 为每个客户端的连接创建一个新的处理单元。
    2. I/O 多路复用: 学习 select(), poll(), epoll (Linux) 等技术,这是构建高性能服务器的关键,它们允许你在一个线程中监控多个套接字,当某个套接字准备好读写时,程序才会去处理它,避免了线程/进程创建和切换的开销。
    3. UDP 编程: 尝试使用 SOCK_DGRAM 实现 UDP 服务器和客户端,你会发现流程比 TCP 简单得多,因为不需要 listen()accept(),直接 sendto()recvfrom() 即可。
    4. 更复杂的应用: 尝试实现一个简单的聊天室、文件传输协议或 HTTP 服务器。

希望这份指南对你有帮助!动手编译和运行上面的代码是理解 Socket 编程最好的方式。

-- 展开阅读全文 --
头像
织梦仿站系列教程第一
« 上一篇 02-23
织梦手机端为何不自动更新?
下一篇 » 02-23
取消
微信二维码
支付宝二维码

目录[+]