Socket编程C语言如何实现高效通信?

99ANYc3cd6
预计阅读时长 36 分钟
位置: 首页 C语言 正文
  1. 核心概念:了解 Socket 是什么,以及通信的基本流程。
  2. 基本 API 函数:介绍最常用的 Socket 函数。
  3. 一个简单的 TCP 服务器/客户端示例:通过代码演示如何实现一个回显服务器。
  4. 一个简单的 UDP 服务器/客户端示例:演示 UDP 通信。
  5. 多线程与 select 模型:如何处理多个客户端连接。
  6. 编译与运行:如何在 Linux 和 Windows 上编译和运行这些代码。

核心概念

什么是 Socket?

Socket 可以看作是网络通信的“终端点”,它就像一个电话插孔,你把电话线(网络连接)插进去,就可以和另一个电话(另一个程序)进行通话。

两种主要的通信方式

  • TCP (Transmission Control Protocol):面向连接的、可靠的通信。
    • 特点:数据传输前需要先建立连接(三次握手),数据传输有序、无丢失、无重复,传输完毕后需要断开连接(四次挥手)。
    • 类比:打电话,你必须先拨号等待对方接听,建立连接后才能说话,说完后挂断。
    • 适用场景:文件传输(HTTP, FTP)、邮件发送(SMTP)等对可靠性要求高的场景。
  • UDP (User Datagram Protocol):无连接的、不可靠的通信。
    • 特点:不需要建立连接,直接发送数据包,速度快,但不保证数据包的顺序、是否丢失或重复。
    • 类比:寄明信片,你写好地址直接扔进邮筒,但不能保证对方一定能收到,也不能保证按寄送顺序收到。
    • 适用场景:视频会议、在线游戏、DNS 查询等对实时性要求高、能容忍少量丢包的场景。

网络字节序

计算机内部存储数据有两种方式:大端序(高位字节在前)和小端序(低位字节在前),不同架构的计算机可能使用不同的字节序,为了确保网络通信中数据能被正确解析,网络协议规定使用 大端序,在发送数据前,如果数据是多字节的(如 int, short),需要将其从主机字节序转换为网络字节序;接收数据后,再转换回主机字节序。

  • htons():Host to Network Short (16位)
  • htonl():Host to Network Long (32位)
  • ntohs():Network to Host Short (16位)
  • ntohl():Network to Host Long (32位)

基本 API 函数 (以 TCP 为例)

服务器端流程

  1. socket(): 创建一个套接字文件描述符。
  2. bind(): 将套接字与一个 IP 地址和端口号绑定。
  3. listen(): 将套接字设置为监听状态,等待客户端连接。
  4. accept(): 接受客户端的连接请求,返回一个新的套接字用于与该客户端通信。
  5. read() / recv(): 从已连接的套接字中读取数据。
  6. write() / send(): 向已连接的套接字中写入数据。
  7. close(): 关闭套接字。

客户端流程

  1. socket(): 创建一个套接字文件描述符。
  2. connect(): 主动连接服务器的 IP 地址和端口号。
  3. read() / recv(): 从已连接的套接字中读取数据。
  4. write() / send(): 向已连接的套接字中写入数据。
  5. close(): 关闭套接字。

TCP 服务器/客户端示例

这是一个经典的“回显”(Echo)服务器:客户端发送什么消息,服务器就原样返回什么消息。

头文件

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h> // 用于 read, write, close
#include <sys/socket.h>
#include <netinet/in.h> // 用于 sockaddr_in
#include <arpa/inet.h>  // 用于 inet_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. 创建 socket 文件描述符
    if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == 0) {
        perror("socket failed");
        exit(EXIT_FAILURE);
    }
    // 2. 设置 socket 选项,允许地址重用
    if (setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &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);
    // 3. 绑定 socket 到地址和端口
    if (bind(server_fd, (struct sockaddr *)&address, sizeof(address)) < 0) {
        perror("bind failed");
        exit(EXIT_FAILURE);
    }
    printf("Server listening on port %d...\n", PORT);
    // 4. 开始监听,等待连接
    if (listen(server_fd, 3) < 0) {
        perror("listen");
        exit(EXIT_FAILURE);
    }
    // 5. 接受客户端连接
    if ((new_socket = accept(server_fd, (struct sockaddr *)&address, (socklen_t*)&addrlen)) < 0) {
        perror("accept");
        exit(EXIT_FAILURE);
    }
    printf("Client connected with IP: %s and Port: %d\n", 
           inet_ntoa(address.sin_addr), ntohs(address.sin_port));
    // 6. 读取客户端数据并发回
    int valread;
    while ((valread = read(new_socket, buffer, BUFFER_SIZE)) > 0) {
        printf("Received from client: %s", buffer);
        send(new_socket, buffer, valread, 0);
        memset(buffer, 0, BUFFER_SIZE); // 清空缓冲区
    }
    // 7. 关闭 socket
    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. 创建 socket
    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);
    // 将 IP 地址从文本转换为网络地址
    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. 关闭 socket
    close(sock);
    return 0;
}

UDP 服务器/客户端示例

UDP 无需连接,流程更简单。

UDP 服务器代码 (udp_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 8081
#define BUFFER_SIZE 1024
int main() {
    int sock;
    struct sockaddr_in serv_addr, cli_addr;
    char buffer[BUFFER_SIZE] = {0};
    int len, n;
    // 1. 创建 UDP socket (SOCK_DGRAM)
    if ((sock = socket(AF_INET, SOCK_DGRAM, 0)) < 0) {
        perror("socket creation failed");
        exit(EXIT_FAILURE);
    }
    memset(&serv_addr, 0, sizeof(serv_addr));
    memset(&cli_addr, 0, sizeof(cli_addr));
    serv_addr.sin_family = AF_INET;
    serv_addr.sin_addr.s_addr = INADDR_ANY;
    serv_addr.sin_port = htons(PORT);
    // 2. 绑定
    if (bind(sock, (const struct sockaddr *)&serv_addr, sizeof(serv_addr)) < 0) {
        perror("bind failed");
        exit(EXIT_FAILURE);
    }
    printf("UDP Server listening on port %d...\n", PORT);
    len = sizeof(cli_addr);
    // 3. 循环接收数据
    while(1) {
        n = recvfrom(sock, (char *)buffer, BUFFER_SIZE, 0, (struct sockaddr *)&cli_addr, &len);
        buffer[n] = '\0';
        printf("Client : %s\n", buffer);
        sendto(sock, (const char *)buffer, n, 0, (struct sockaddr *)&cli_addr, len);
        printf("Echoed back to client\n");
    }
    return 0;
}

UDP 客户端代码 (udp_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 8081
#define BUFFER_SIZE 1024
int main() {
    int sock;
    struct sockaddr_in serv_addr;
    char *hello = "Hello from UDP client";
    char buffer[BUFFER_SIZE] = {0};
    struct sockaddr_in serv_addr_len;
    // 1. 创建 UDP socket
    if ((sock = socket(AF_INET, SOCK_DGRAM, 0)) < 0) {
        perror("socket creation failed");
        exit(EXIT_FAILURE);
    }
    memset(&serv_addr, 0, sizeof(serv_addr));
    serv_addr.sin_family = AF_INET;
    serv_addr.sin_port = htons(PORT);
    if (inet_pton(AF_INET, "127.0.0.1", &serv_addr.sin_addr) <= 0) {
        perror("invalid address/ Address not supported");
        exit(EXIT_FAILURE);
    }
    // 2. 发送数据 (不需要 connect)
    sendto(sock, (const char *)hello, strlen(hello), 0, (const struct sockaddr *)&serv_addr, sizeof(serv_addr));
    printf("Hello message sent.\n");
    // 3. 接收服务器返回的数据
    int n;
    socklen_t len = sizeof(serv_addr);
    n = recvfrom(sock, (char *)buffer, BUFFER_SIZE, 0, (struct sockaddr *)&serv_addr, &len);
    buffer[n] = '\0';
    printf("Server : %s\n", buffer);
    close(sock);
    return 0;
}

处理多个客户端

上面的 TCP 服务器一次只能处理一个客户端,要处理多个客户端,有几种常见模型:

  • 多线程/多进程模型:每当 accept() 一个新客户端,就创建一个新的线程或进程来处理与该客户端的通信,主线程则继续监听新的连接,这是最直观的方式。
  • I/O 多路复用模型 (select/poll/epoll):使用一个线程来管理所有套接字。select 函数会阻塞,直到有一个或多个套接字准备好进行 I/O 操作,当 select 返回后,程序遍历所有套接字,对“就绪”的套接字进行读写操作。epoll 是 Linux 下更高效的 I/O 多路复用机制。

select 模型简单示例 (伪代码):

fd_set read_fds;
int max_fd = server_socket;
while(1) {
    FD_ZERO(&read_fds); // 清空集合
    FD_SET(server_socket, &read_fds); // 添加监听 socket
    // ... 将所有客户端 socket 也添加到 read_fds 中
    // 并更新 max_fd
    select(max_fd + 1, &read_fds, NULL, NULL, NULL); // 阻塞等待
    if (FD_ISSET(server_socket, &read_fds)) {
        // 有新连接,执行 accept
    }
    // ... 遍历所有客户端 socket
    // FD_ISSET(client_socket, &read_fds) 为真,则进行 read
}

编译与运行

Linux / macOS

使用 gcc 编译,需要链接 -lpthread 如果使用了多线程。

# 编译 TCP 服务器和客户端
gcc server.c -o server
gcc client.c -o client
# 编译 UDP 服务器和客户端
gcc udp_server.c -o udp_server
gcc udp_client.c -o udp_client
# 运行
# 1. 打开一个终端,运行服务器
./server
# 2. 打开另一个终端,运行客户端
./client

Windows

Windows 使用不同的头文件和库。

头文件:

#include <winsock2.h>
#include <ws2tcpip.h>
#pragma comment(lib, "ws2_32.lib") // 链接 Winsock 库

�和清理: 在程序开始时需要初始化 Winsock:

WSADATA wsaData;
if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0) {
    printf("WSAStartup failed\n");
    return 1;
}

在程序结束时需要清理:

WSACleanup();

socket(), bind(), listen(), accept() 等函数在 Windows 下的名称和参数与 Linux 基本一致,但返回的错误码处理方式不同(通常使用 WSAGetLastError())。

编译: 使用 Visual Studio 的 cl.exe 或 MinGW 的 gcc。

# 使用 MinGW
gcc server.c -o server.exe -lws2_32
gcc client.c -o client.exe -lws2_32

希望这个详细的教程能帮助你入门 C 语言 Socket 编程!

-- 展开阅读全文 --
头像
dede如何整合uc?
« 上一篇 今天
arduino和c语言
下一篇 » 今天

相关文章

取消
微信二维码
支付宝二维码

目录[+]