c语言socket编程实例

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

核心概念

在开始编码前,先理解几个核心概念:

  1. Socket (套接字):可以看作是网络通信的端点,它是一个文件描述符,程序可以通过它来发送和接收数据。
  2. IP 地址:网络上设备的唯一标识,0.0.1 (本机地址)。
  3. 端口号:同一台主机上,不同应用程序的标识,一个 IP 地址可以对应多个端口,从而区分不同的服务。
  4. 协议:网络通信的规则,我们主要使用 TCP (流式套接字)UDP (数据报套接字)
    • TCP (Transmission Control Protocol):面向连接的、可靠的协议,数据像水流一样,有序、无丢失地到达,适合要求高可靠性的场景,如文件传输、网页浏览。
    • UDP (User Datagram Protocol):无连接的、不可靠的协议,数据像一个个包裹,发送出去但不保证到达或顺序,适合要求速度、能容忍少量丢包的场景,如视频会议、在线游戏。
  5. 字节序:计算机内存中多字节数据的存储顺序,网络协议规定使用大端序,而大多数 x86/x64 架构的电脑使用小端序,在发送包含多字节数据(如端口号、IP地址)时,需要进行转换,Linux/Unix 系统提供了 htons() (host to network short), htonl() (host to network long) 等函数来完成这个转换。

简单的 TCP 回显服务器与客户端

这个例子是最经典的入门程序,客户端发送一条消息,服务器原样返回这条消息。

服务器端代码 (server.c)

服务器的工作流程是:

  1. 创建套接字 (socket)。
  2. 绑定 IP 地址和端口号 (bind)。
  3. 监听连接 (listen)。
  4. 接受客户端连接 (accept)。
  5. 与客户端收发数据 (send/recv)。
  6. 关闭连接 (close)。
#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, &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. 绑定套接字到指定的 IP 和端口
    if (bind(server_fd, (struct sockaddr *)&address, sizeof(address)) < 0) {
        perror("bind failed");
        exit(EXIT_FAILURE);
    }
    // 3. 开始监听连接
    if (listen(server_fd, 3) < 0) { // backlog=3, 允许的排队连接数
        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 = read(new_socket, buffer, BUFFER_SIZE);
    printf("Client: %s\n", buffer);
    // 6. 将数据回写给客户端
    send(new_socket, buffer, valread, 0);
    printf("Message sent back to client.\n");
    // 7. 关闭套接字
    close(new_socket);
    close(server_fd);
    return 0;
}

客户端代码 (client.c)

客户端的工作流程是:

  1. 创建套接字 (socket)。
  2. 连接到服务器 (connect)。
  3. 发送数据 (send)。
  4. 接收数据 (recv)。
  5. 关闭连接 (close)。
#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: %s\n", buffer);
    // 5. 关闭套接字
    close(sock);
    return 0;
}

如何编译和运行

  1. 保存代码:将上面的代码分别保存为 server.cclient.c

  2. 编译:打开终端,使用 gcc 进行编译。

    # 编译服务器
    gcc server.c -o server
    # 编译客户端
    gcc client.c -o client
  3. 运行

    • 第一步:在一个终端中运行服务器。
      ./server

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

    • 第二步:在另一个终端中运行客户端。
      ./client

      你会看到客户端的输出:

      Connected to server.
      Hello message sent
      Server: Hello from client
    • 第三步:回到服务器终端,你会看到服务器的输出:
      Server listening on port 8080...
      Connection accepted from 127.0.0.1:54321 (端口号可能不同)
      Client: Hello from client
      Message sent back to client.

支持多客户端的服务器(使用 fork

上面的服务器只能处理一个客户端连接,当处理客户端时,它无法接受新的连接,一个简单的解决方案是使用 fork 系统调用,为每个客户端连接创建一个子进程来处理。

服务器端代码 (multi_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>
#include <sys/wait.h> // for waitpid
#define PORT 8081
#define BUFFER_SIZE 1024
void handle_client(int client_socket) {
    char buffer[BUFFER_SIZE] = {0};
    int valread;
    // 读取客户端消息
    valread = read(client_socket, buffer, BUFFER_SIZE);
    printf("Client %d: %s\n", client_socket, buffer);
    // 回显消息
    send(client_socket, buffer, valread, 0);
    printf("Echoed back to client %d\n", client_socket);
    // 关闭客户端套接字
    close(client_socket);
    exit(0); // 子进程完成任务后退出
}
int main() {
    int server_fd, new_socket;
    struct sockaddr_in address;
    int opt = 1;
    int addrlen = sizeof(address);
    // 1. 创建套接字
    if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == 0) {
        perror("socket failed");
        exit(EXIT_FAILURE);
    }
    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, 10) < 0) { // backlog=10
        perror("listen");
        exit(EXIT_FAILURE);
    }
    printf("Multi-process server listening on port %d...\n", PORT);
    while (1) {
        // 4. 接受连接
        if ((new_socket = accept(server_fd, (struct sockaddr *)&address, (socklen_t*)&addrlen)) < 0) {
            perror("accept");
            continue; // 继续接受下一个连接
        }
        printf("New connection accepted, socket fd: %d\n", new_socket);
        // 5. 创建子进程来处理客户端
        pid_t pid = fork();
        if (pid < 0) {
            perror("fork failed");
            close(new_socket);
        } else if (pid == 0) { // 子进程
            close(server_fd); // 子进程不需要监听套接字
            handle_client(new_socket); // 处理客户端
        } else { // 父进程
            close(new_socket); // 父进程不需要已连接的套接字
            // 回收僵尸进程
            waitpid(-1, NULL, WNOHANG);
        }
    }
    return 0;
}

如何运行: 编译和运行方式与之前类似,你可以打开多个终端窗口,运行多个 ./client,每个客户端都能得到服务器的响应,服务器主进程会持续运行,并为每个新连接创建一个子进程。


总结与注意事项

  1. 错误处理:Socket 编程中,几乎所有系统调用都可能失败。检查它们的返回值至关重要,本例中使用了 perror 来打印错误信息,这是调试的好习惯。
  2. 头文件
    • <sys/socket.h>: 核心 Socket API。
    • <netinet/in.h>: IP 地址和端口号的结构定义(sockaddr_in)以及字节序转换函数。
    • <arpa/inet.h>: inet_ptoninet_ntop 函数,用于 IP 地址的转换。
    • <unistd.h>: 提供 read, write, close, fork 等函数。
  3. 文件描述符:在 Linux/Unix 中,Socket 被视为一种文件,可以使用 read()write() 来收发数据,也可以使用 recv()send(),它们提供了更多选项。
  4. 资源释放:程序结束时,一定要记得 close() 所有打开的套接字,否则会导致文件描述符泄露。
  5. 防火墙:如果在真实机器上运行,请确保防火墙允许你使用的端口号(如 8080)的流量。
  6. 跨平台:本代码主要针对 Linux/Unix 环境,在 Windows 上,需要包含 <winsock2.h> 并在使用前调用 WSAStartup() 初始化 Winsock,代码会有所不同。

这个实例涵盖了 C 语言 Socket 编程的核心要点,掌握了这些,你就可以开始构建自己的网络应用程序了。

-- 展开阅读全文 --
头像
织梦会员空间列表分页
« 上一篇 今天
织梦首页调用文章视频
下一篇 » 今天

相关文章

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

目录[+]