凌峰创科服务平台

C Socket服务器客户端如何通信?

核心概念

在开始之前,我们需要理解几个核心概念:

C Socket服务器客户端如何通信?-图1
(图片来源网络,侵删)
  1. Socket (套接字):它就像一个电话插座,是网络通信的端点,程序通过 Socket 发送和接收数据。
  2. IP 地址:网络上设备的唯一地址,服务端需要绑定到一个 IP 地址上,以便客户端知道连接到哪里。
  3. 端口号:IP 地址标识了计算机,端口号则标识了计算机上的具体应用程序(服务),Web 服务通常使用 80 端口。
  4. TCP (传输控制协议):一种面向连接的、可靠的协议,在通信前,客户端和服务器必须先建立一个连接(三次握手),之后数据可以按顺序、无差错地传输。
  5. 流程
    • 服务器:创建 Socket -> 绑定 IP 和端口 -> 监听连接 -> 接受客户端连接 -> 与客户端收发数据 -> 关闭连接。
    • 客户端:创建 Socket -> 连接服务器 -> 与服务器收发数据 -> 关闭连接。

准备工作:编译和链接

在 Linux/macOS 系统上,Socket 相关的函数位于 libsocket 库中,通常不需要额外链接,但在 Windows 上,需要链接 Ws2_32.lib

  • Linux/macOS 编译命令
    gcc server.c -o server
    gcc client.c -o client
  • Windows 编译命令 (使用 MinGW)
    gcc server.c -o server.exe -lws2_32
    gcc client.c -o client.exe -lws2_32

第一部分:服务器端代码

服务器的主要任务是“等待”并“响应”客户端的请求。

代码逻辑:

  1. 创建套接字 (socket)
  2. 绑定地址和端口 (bind):告诉操作系统这个 Socket 使用的 IP 和端口。
  3. 监听连接 (listen):将 Socket 设置为监听模式,等待客户端连接。
  4. 接受连接 (accept):从等待队列中取出一个客户端连接请求,并创建一个新的 Socket 专门与这个客户端通信。
  5. 收发数据 (send/recv):通过新创建的 Socket 与客户端进行数据交互。
  6. 关闭套接字 (close):关闭通信套接字和监听套接字。

server.c

C Socket服务器客户端如何通信?-图2
(图片来源网络,侵删)
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h> // 用于 read, write, close
#include <sys/socket.h> // 用于 socket, bind, listen, accept
#include <netinet/in.h> // 用于 struct sockaddr_in
#include <arpa/inet.h> // 用于 inet_addr
#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);
    }
    // 2. 绑定地址和端口
    address.sin_family = AF_INET; // IPv4
    address.sin_addr.s_addr = INADDR_ANY; // 监听所有可用的网络接口
    address.sin_port = htons(PORT); // 将端口号从主机字节序转换为网络字节序
    // 绑定套接字到地址和端口
    if (bind(server_fd, (struct sockaddr *)&address, sizeof(address)) < 0) {
        perror("bind failed");
        exit(EXIT_FAILURE);
    }
    printf("Server bound on port %d\n", PORT);
    // 3. 监听连接
    // SOMAXCONN 是系统允许的最大连接请求数
    if (listen(server_fd, SOMAXCONN) < 0) {
        perror("listen");
        exit(EXIT_FAILURE);
    }
    printf("Server is listening for connections...\n");
    // 4. 接受连接
    // accept 会阻塞,直到有客户端连接
    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;
    // 从客户端读取数据
    valread = read(new_socket, buffer, BUFFER_SIZE);
    printf("Client says: %s\n", buffer);
    // 向客户端发送数据
    char *response = "Hello from server!";
    send(new_socket, response, strlen(response), 0);
    printf("Hello message sent\n");
    // 6. 关闭套接字
    close(new_socket);
    close(server_fd);
    return 0;
}

第二部分:客户端代码

客户端的主要任务是“发起”连接并与服务器“通信”。

代码逻辑:

  1. 创建套接字 (socket)
  2. 连接服务器 (connect):指定服务器的 IP 地址和端口号,发起连接请求。
  3. 收发数据 (send/recv):通过已连接的 Socket 与服务器进行数据交互。
  4. 关闭套接字 (close)

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. 连接服务器
    // connect 会阻塞,直到连接成功或失败
    if (connect(sock, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) < 0) {
        printf("\nConnection Failed \n");
        return -1;
    }
    printf("Connected to server on port %d\n", PORT);
    // 3. 向服务器发送数据
    send(sock, hello, strlen(hello), 0);
    printf("Hello message sent\n");
    // 4. 从服务器读取数据
    int valread = read(sock, buffer, BUFFER_SIZE);
    printf("Server says: %s\n", buffer);
    // 5. 关闭套接字
    close(sock);
    return 0;
}

第三部分:如何运行和测试

  1. 编译代码:分别编译服务器和客户端。

    C Socket服务器客户端如何通信?-图3
    (图片来源网络,侵删)
    gcc server.c -o server
    gcc client.c -o client
  2. 启动服务器:在一个终端窗口中运行服务器,它会启动并等待连接。

    ./server

    你会看到输出:

    Server bound on port 8080
    Server is listening for connections...
  3. 启动客户端:在另一个终端窗口中运行客户端,客户端会连接到服务器。

    ./client

    你会看到客户端的输出:

    Connected to server on port 8080
    Hello message sent
    Server says: Hello from server!
  4. 观察服务器终端:在服务器终端,你会看到它接受了连接并收到了消息。

    Server bound on port 8080
    Server is listening for connections...
    Connection accepted from 127.0.0.1:54321  // 端口号可能不同
    Client says: Hello from client
    Hello message sent

第四部分:代码解释与重要函数

头文件

  • <sys/socket.h>:核心 Socket 函数。
  • <netinet/in.h>:定义了 sockaddr_in 结构体,用于处理 IPv4 地址和端口。
  • <arpa/inet.h>:提供了 IP 地址转换函数,如 inet_addr (已弃用) 和 inet_pton (推荐)。

关键函数和结构体

  1. socket(int domain, int type, int protocol)

    • domain: AF_INET (IPv4), AF_INET6 (IPv6), AF_UNIX (本地域)。
    • type: SOCK_STREAM (TCP), SOCK_DGRAM (UDP)。
    • protocol: 通常设为 0,让系统自动选择。
  2. *`bind(int sockfd, const struct sockaddr addr, socklen_t addrlen)`**

    • sockfd: socket() 返回的文件描述符。
    • addr: 指向 sockaddr 结构体的指针,对于 IPv4,我们通常使用 sockaddr_in 并将其地址强制转换为 sockaddr*
    • sockaddr_in 结构体:
      • sin_family: 地址族,设为 AF_INET
      • sin_port: 16位端口号。必须用 htons() 转换为网络字节序(大端序)。
      • sin_addr: 32位 IP 地址。INADDR_ANY 表示监听所有网络接口。inet_pton() 可将点分十进制字符串(如 "127.0.0.1")转换为网络字节序。
  3. listen(int sockfd, int backlog)

    • sockfd: 已绑定的 Socket。
    • backlog: 请求队列的最大长度。
  4. int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen)

    • sockfd: 处于监听状态的 Socket。
    • addraddrlen: 用于保存客户端的地址信息,如果不关心,可以设为 NULL
    • 返回值:一个新的 Socket 文件描述符,专门用于与这个已连接的客户端通信,原来的监听 Socket 继续监听新的连接请求。
  5. *`connect(int sockfd, const struct sockaddr addr, socklen_t addrlen)`**

    • sockfd: 客户端创建的 Socket。
    • addr: 指向服务器地址结构体的指针。
  6. ssize_t read(int fd, void *buf, size_t count)ssize_t recv(int sockfd, void *buf, size_t len, int flags)

    • 从文件描述符 fd 或已连接的 Socket sockfd 读取最多 count/len 字节的数据到 buf 中。
    • read 是通用 I/O 函数,recv 是专用于 Socket 的函数,可以设置 flags (如 MSG_WAITALL)。
    • 如果对端关闭了连接,read/recv 会返回 0。
  7. ssize_t write(int fd, const void *buf, size_t count)ssize_t send(int sockfd, const void *buf, size_t len, int flags)

    • buf 中的最多 count/len 字节数据写入文件描述符 fd 或已连接的 Socket sockfd
    • write 是通用 I/O 函数,send 是专用于 Socket 的函数,可以设置 flags (如 MSG_DONTROUTE)。
  8. close(int fd)

    关闭文件描述符或 Socket,释放资源。

总结与进阶

  • 阻塞 vs. 非阻塞:上述代码中的 accept, connect, read, recv 都是阻塞的,意味着程序会暂停执行,直到操作完成,在实际应用中,你可能需要使用 select, poll, 或 epoll (Linux) 等机制来实现 I/O 多路复用,以便服务器能同时处理多个客户端。
  • 错误处理:代码中的 perror 是一个简单的错误处理方式,在生产环境中,你可能需要更健壮的错误恢复机制。
  • 并发服务器:要实现一个可以同时与多个客户端通信的服务器,最简单的方式是在 accept 之后创建一个新线程或新进程来处理与该客户端的通信,而主线程则继续调用 accept 等待下一个连接。
分享:
扫描分享到社交APP
上一篇
下一篇