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

99ANYc3cd6
预计阅读时长 34 分钟
位置: 首页 C语言 正文
  1. 核心概念:理解什么是 Socket,以及通信的基本流程。
  2. 核心函数:介绍在 C 语言中使用 Socket 所需的主要函数。
  3. 一个完整的例子:分别实现一个简单的 TCP 服务器和客户端。
  4. UDP 编程简介:对比 TCP,介绍 UDP 的简单例子。
  5. 重要注意事项:如编译、错误处理、多客户端等。

核心概念

什么是 Socket?

你可以把 Socket 想象成一个“网络上的文件描述符”,在 C 语言中,文件描述符是一个整数,代表一个打开的文件、管道或 Socket,通过这个“句柄”,你可以对它进行读写操作,就像读写本地文件一样,只不过数据是通过网络传输的。

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

通信模型(TCP)

我们以最常用的 TCP (传输控制协议) 为例,它提供了面向连接的、可靠的、基于字节流的服务。

服务器端流程

  1. 创建套接字 (socket):创建一个新的通信端点。
  2. 绑定地址和端口 (bind):将套接字与一个特定的 IP 地址和端口号绑定,这样客户端才能找到它。
  3. 监听连接 (listen):让套接字进入被动监听状态,等待客户端的连接请求。
  4. 接受连接 (accept):从等待队列中取出一个已完成的连接请求,创建一个新的套接字与该客户端进行通信,原来的监听套接字继续监听新的连接。
  5. 收发数据 (send/recv):通过新创建的套接字与客户端进行数据交互。
  6. 关闭套接字 (close):通信结束后,关闭套接字,释放资源。

客户端流程

  1. 创建套接字 (socket):创建一个新的通信端点。
  2. 连接服务器 (connect):主动向服务器的 IP 地址和端口号发起连接请求。
  3. 收发数据 (send/recv):连接建立后,通过套接字与服务器进行数据交互。
  4. 关闭套接字 (close):通信结束后,关闭套接字。

一个形象的比喻

c 语言 socket
(图片来源网络,侵删)
  • 服务器:就像一个电话总机(bind + listen),总机本身不直接与外部通话,它负责接听所有来电。
  • 客户端:就像一个打电话的人(connect),拨打总机的号码。
  • accept:总机接线员拿起一个电话,分配一个分机给这个来电者,从此,这个分机(新的 socket)就专门负责与这个来电者通话,而总机可以继续接听下一个电话。

核心函数

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

#include <sys/socket.h>   // 核心 Socket 函数
#include <netinet/in.h>   // IP 地址和端口结构体
#include <arpa/inet.h>    // IP 地址转换函数 (inet_pton)
#include <unistd.h>       // close 函数
#include <string.h>      // memset, strlen 函数
#include <stdio.h>        // perror, printf 函数

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,系统会自动根据 type 选择。
  • 返回值:成功返回一个套接字文件描述符(一个整数),失败返回 -1。

bind()

将套接字与一个 IP 地址和端口号绑定。

int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
  • sockfd: socket() 返回的套接字描述符。
  • addr: 指向 sockaddr 结构体的指针,包含了要绑定的地址和端口。
  • addrlen: addr 结构体的长度。
  • sockaddr_in 结构体 (用于 IPv4):
    struct sockaddr_in {
        short            sin_family;   // 地址族,AF_INET
        unsigned short   sin_port;     // 端口号,需要用 htons() 转换
        struct in_addr  sin_addr;     // IP 地址,用 inet_pton() 填充
        char             sin_zero[8];  // 填充0,使结构体大小与 sockaddr 一致
    };
  • htons() 函数:Host to Network Short,将主机字节序的端口号转换为网络字节序(大端序),网络字节序是大端序。

listen()

让套接字进入监听状态,准备接受连接。

int listen(int sockfd, int backlog);
  • sockfd: 已绑定的套接字描述符。
  • backlog: 等待连接队列的最大长度。
  • 返回值:成功返回 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: 指向服务器 sockaddr 结构体的指针。
  • addrlen: addr 结构体的长度。
  • 返回值:成功返回 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: 已连接的套接字描述符(服务器端是 accept() 返回的,客户端是 connect() 使用的)。
  • buf: 数据缓冲区。
  • len: 要发送/接收的数据长度。
  • flags: 通常设为 0。
  • 返回值:成功返回实际发送/接收的字节数;对于 recv(),如果对方关闭了连接,返回 0;失败返回 -1。

close()

关闭套接字,释放资源。

int close(int sockfd);

完整的 TCP 例子

服务器端代码 (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. 创建套接字
    if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == 0) {
        perror("socket failed");
        exit(EXIT_FAILURE);
    }
    // 设置套接字选项,允许地址重用(避免 TIME_WAIT 状态导致端口占用)
    if (setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt))) {
        perror("setsockopt");
        exit(EXIT_FAILURE);
    }
    address.sin_family = AF_INET;
    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("Connection accepted from %s:%d\n", inet_ntoa(address.sin_addr), ntohs(address.sin_port));
    // 5. 收发数据
    int valread;
    while(1) {
        valread = recv(new_socket, buffer, BUFFER_SIZE, 0);
        if (valread <= 0) {
            // valread == 0 表示客户端关闭了连接
            // valread < 0 表示发生错误
            break;
        }
        printf("Client: %s", buffer);
        // 回显消息给客户端
        send(new_socket, buffer, valread, 0);
        memset(buffer, 0, BUFFER_SIZE); // 清空缓冲区
    }
    // 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 = recv(sock, buffer, BUFFER_SIZE, 0);
    printf("Server: %s\n", buffer);
    // 5. 关闭套接字
    close(sock);
    return 0;
}

如何编译和运行

  1. 打开两个终端。
  2. 在第一个终端编译并运行服务器:
    gcc server.c -o server
    ./server

    你会看到 "Server listening on port 8080..."。

  3. 在第二个终端编译并运行客户端:
    gcc client.c -o client
    ./client

    客户端会连接服务器,发送一条消息,并打印出服务器的回显,服务器终端也会显示收到的消息。


UDP 编程简介

UDP 是无连接的、不可靠的、基于数据报的协议,它的编程模型比 TCP 简单,没有 listen()accept() 步骤。

服务器端流程

  1. socket()
  2. bind() (绑定到端口)
  3. 进入一个循环,使用 recvfrom() 接收数据,sendto() 发送数据。

客户端流程

  1. socket()
  2. 进入一个循环,使用 sendto() 发送数据,recvfrom() 接收数据。

UDP 服务器端示例 (udp_server.c)

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#define PORT 8080
#define BUFFER_SIZE 1024
int main() {
    int sockfd;
    char buffer[BUFFER_SIZE];
    struct sockaddr_in servaddr, cliaddr;
    // 创建 UDP 套接字
    if ((sockfd = socket(AF_INET, SOCK_DGRAM, 0)) < 0 ) {
        perror("socket creation failed");
        exit(EXIT_FAILURE);
    }
    memset(&servaddr, 0, sizeof(servaddr));
    memset(&cliaddr, 0, sizeof(cliaddr));
    servaddr.sin_family = AF_INET;
    servaddr.sin_addr.s_addr = INADDR_ANY;
    servaddr.sin_port = htons(PORT);
    // 绑定
    if ( bind(sockfd, (const struct sockaddr *)&servaddr, sizeof(servaddr)) < 0 ) {
        perror("bind failed");
        exit(EXIT_FAILURE);
    }
    printf("UDP Server waiting for messages...\n");
    int len, n;
    len = sizeof(cliaddr); // 重要:len 必须初始化
    // 循环接收数据
    while(1) {
        n = recvfrom(sockfd, (char *)buffer, BUFFER_SIZE, 0, ( struct sockaddr *) &cliaddr, &len);
        buffer[n] = '\0';
        printf("Client : %s\n", buffer);
        sendto(sockfd, (const char *)buffer, strlen(buffer), 0, (const struct sockaddr *) &cliaddr, len);
        printf("Hello message sent.\n");
    }
    return 0;
}

重要注意事项

  1. 编译时链接库:在 Linux/macOS 上,上面的例子可以直接用 gcc 编译,因为系统调用已经包含在标准库中,但在某些系统(如较旧的 Solaris)或 Windows 上,可能需要显式链接 -lnsl -lsocket
    # Windows 下可能需要
    # gcc server.c -o server -lws2_32
  2. 错误处理:所有 Socket 函数都可能失败,必须检查它们的返回值,并根据错误码(perror 是一个好帮手)进行相应处理,忘记检查返回值是初学者最常见的错误。
  3. 字节序:网络字节序是大端序,在设置端口号时,必须使用 htons() (Host to Network Short) 或 htonl() (Host to Network Long) 进行转换,IP 地址通过 inet_pton() 函数处理,它会自动处理字节序问题。
  4. 多客户端:上面的 TCP 服务器一次只能处理一个客户端,当 accept() 返回后,原服务器套接字可以继续监听,而新套接字负责与客户端通信,要实现真正的并发服务器,需要使用多线程 (pthread) 或多进程 (fork)。
  5. 阻塞与非阻塞:默认情况下,accept(), recv(), connect() 等函数是阻塞的,程序会一直等待直到事件发生,可以使用 fcntl()ioctl() 将套接字设置为非阻塞模式,或者使用 select(), poll(), epoll 等多路复用技术来管理多个套接字。
-- 展开阅读全文 --
头像
dede简介模板下载在哪里找?
« 上一篇 04-22
c语言 switch
下一篇 » 04-22
取消
微信二维码
支付宝二维码

目录[+]