C语言socket编程怎么快速入门?

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

目录

  1. 引言:什么是 Socket?
  2. Socket 编程基础概念
    • 网络字节序
    • 套接字类型
    • 基本流程
  3. 核心 API 函数详解
    • socket(): 创建套接字
    • bind(): 绑定地址和端口
    • listen(): 监听连接 (服务器)
    • accept(): 接受连接 (服务器)
    • connect(): 发起连接 (客户端)
    • send() / recv(): 发送和接收数据
    • close(): 关闭套接字
  4. 实战案例:简单的回显服务器与客户端
    • 服务器端代码 (server.c)
    • 客户端代码 (client.c)
    • 如何编译和运行
  5. 进阶主题与注意事项
    • recv() 的阻塞问题
    • 使用 select() 实现多路复用
    • 错误处理
    • 代码可移植性 (Windows)
  6. 总结与资源

引言:什么是 Socket?

你可以把 Socket(套接字) 想象成一个通信的“终端”或“插座”,它是一组接口,是应用程序与网络协议栈之间的桥梁,通过 Socket,一个程序可以发送或接收数据,这些数据可以在同一台计算机的不同进程间传输,也可以在网络中不同计算机的进程间传输。

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

Socket 编程就是利用这些接口来实现网络通信的编程方式,它是最底层的网络通信方式之一,是构建更高级网络服务(如 HTTP, FTP, SMTP)的基础。

Socket 编程基础概念

网络字节序

计算机在内存中存储多字节数据(如 int, long)时有两种方式:

  • 大端序: 高位字节存储在低地址,低位字节存储在高地址。
  • 小端序: 低位字节存储在低地址,高位字节存储在高地址。

不同的操作系统可能使用不同的字节序,为了确保网络通信中数据能被正确解析,网络协议规定统一使用大端序,在发送数据前,如果主机是小端序,需要将数据转换成网络字节序;在接收数据后,如果主机是小端序,需要转换回主机字节序。

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

套接字类型

主要有三种类型:

c语言socket编程教程
(图片来源网络,侵删)
  1. 流式套接字: SOCK_STREAM

    • 特点: 面向连接,提供可靠的、有序的、双向的字节流服务。
    • 协议: 通常基于 TCP (Transmission Control Protocol)。
    • 适用场景: 要求数据不丢失、不重复、按序到达的场景,如文件传输、网页浏览。
  2. 数据报套接字: SOCK_DGRAM

    • 特点: 无连接,提供“尽力而为”的数据报服务,不保证顺序,不保证不丢失,也不保证不重复。
    • 协议: 通常基于 UDP (User Datagram Protocol)。
    • 适用场景: 对实时性要求高,能容忍少量丢包的场景,如视频会议、在线游戏、DNS查询。
  3. 原始套接字: SOCK_RAW

    • 特点: 可以直接访问底层协议(如 IP, ICMP),可以构造自己的数据包。
    • 适用场景: 网络诊断、安全工具开发等,通常需要 root 权限。

基本流程

网络通信模型分为服务器端和客户端,它们的流程是不同的。

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

服务器端流程 (TCP为例):

  1. socket() -> 创建套接字
  2. bind() -> 绑定 IP 地址和端口号
  3. listen() -> 开始监听,等待客户端连接
  4. accept() -> 阻塞,等待并接受客户端连接,返回一个新的套接字用于通信
  5. send() / recv() -> 与客户端通过新套接字收发数据
  6. close() -> 关闭通信套接字
  7. (可选) close() -> 关闭监听套接字

客户端流程 (TCP为例):

  1. socket() -> 创建套接字
  2. connect() -> 主动连接服务器的 IP 地址和端口号
  3. send() / recv() -> 与服务器收发数据
  4. close() -> 关闭套接字

核心 API 函数详解

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

#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, htons 等函数
#include <errno.h>      // 提供 errno

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 结构体的指针,实际编程中,我们更常用 sockaddr_in 结构体,并通过指针转换。
    struct sockaddr_in {
        short            sin_family;   // 地址族, AF_INET
        unsigned short   sin_port;     // 端口号, 需用 htons() 转换
        struct in_addr  sin_addr;     // IP 地址
        char             sin_zero[8]; // 填充字段, 保持与 sockaddr 结构体大小一致
    };
    struct in_addr {
        unsigned long s_addr; // IP 地址, 用 inet_addr() 或 inet_pton() 转换
    };
  • addrlen: addr 结构体的长度,sizeof(struct sockaddr_in)
  • 返回值: 成功返回 0,失败返回 -1。

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_in 结构体的指针。
  • addrlen: addr 结构体的长度。
  • 返回值: 成功返回 0,失败返回 -1。

send() / recv()

用于通过已连接的套接字发送和接收数据,它们是对 write()read() 的封装,提供了更多功能。

// 发送数据
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。
    • MSG_OOB: 处理带外数据。
    • MSG_PEEK: 查看数据但不从缓冲区移除。
  • 返回值:
    • 成功时,send() 返回实际发送的字节数,recv() 返回实际接收到的字节数。
    • 如果返回 0,表示连接已关闭(对 recv() 有效)。
    • 如果返回 -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);
    }
    // 设置套接字选项,允许地址重用
    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);
    // 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 *message = "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);
    // 将 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;
    }
    // 3. 发送数据
    send(sock, message, strlen(message), 0);
    printf("Message sent: %s\n", message);
    // 4. 接收服务器回显
    int valread = read(sock, buffer, BUFFER_SIZE);
    printf("Server echoed: %s\n", buffer);
    // 5. 关闭套接字
    close(sock);
    return 0;
}

如何编译和运行

  1. 保存代码: 将上述两段代码分别保存为 server.cclient.c

  2. 编译: 打开两个终端,分别编译服务器和客户端。

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

    • 在终端 1 中运行服务器:
      ./server

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

    • 在终端 2 中运行客户端:
      ./client

      客户端会发送消息并打印回显,然后退出,服务器会打印客户端连接和断开的信息。

进阶主题与注意事项

recv() 的阻塞问题

recv() (和 read()) 在没有数据可读时会阻塞,即程序会暂停执行,直到有数据到达或连接关闭,这在简单的例子中没问题,但在复杂的程序中,我们不希望一个连接阻塞整个程序,解决方法包括:

  • 多线程/多进程: 为每个客户端连接创建一个新的线程或进程。
  • I/O 多路复用: 使用 select(), poll()epoll() (Linux) 同时监视多个套接字,哪个套接字准备好读写,就处理哪个,这是高性能服务器的常用技术。

使用 select() 实现多路复用

select() 允许你监视一组文件描述符(包括套接字),等待其中一个变为“就绪”状态(可读、可写或出现异常)。

// 伪代码示例
fd_set read_fds;
int max_fd;
// 初始化套接字列表和 max_fd
// ...
while (1) {
    FD_ZERO(&read_fds); // 清空集合
    FD_SET(server_sock, &read_fds); // 添加监听套接字
    // ... 添加所有客户端套接字
    max_fd = server_sock; // 找出最大的套接字描述符
    select(max_fd + 1, &read_fds, NULL, NULL, NULL); // 阻塞等待
    if (FD_ISSET(server_sock, &read_fds)) {
        // 有新连接
        int new_sock = accept(server_sock, ...);
        // 将 new_sock 加入你的套接字列表
    }
    for (int i = 0; i <= max_fd; i++) {
        if (FD_ISSET(i, &read_fds)) {
            // 有客户端数据可读
            int valread = read(i, buffer, BUFFER_SIZE);
            if (valread == 0) {
                // 客户端断开连接
                close(i);
                // 从列表中移除 i
            } else {
                // 处理数据
            }
        }
    }
}

错误处理

网络编程充满了不确定性,必须对每个可能失败的系统调用进行检查,并根据错误类型(errno)采取相应措施。accept() 可能被信号中断,返回 EINTR 错误,此时可以重试。

代码可移植性 (Windows)

Windows 下的 Socket 编程与 Linux 有显著不同:

  1. 头文件: 需要包含 <winsock2.h><ws2tcpip.h>
  2. : 需要链接 ws2_32.lib 库。
  3. �化: 在使用任何 Socket 函数前,必须调用 WSAStartup() 初始化 Winsock。
  4. 清理: 程序结束时,必须调用 WSACleanup()
  5. 数据类型: SOCKET 类型,而不是 int
  6. 关闭: 使用 closesocket() 而不是 close()

为了简化跨平台开发,可以使用第三方库如 libuvBoost.Asio,它们封装了底层的差异。

总结与资源

Socket 编程是网络开发的基石,本教程涵盖了 TCP Socket 编程的核心流程和 API,并通过一个简单的回显示例展示了其应用,关键点在于理解服务器和客户端的不同角色,以及 socket, bind, listen, accept, connect, send, recv 这些函数的协同工作方式。

进一步学习资源:

  • 书籍:
    • 《Unix 网络编程 卷1:套接字联网API》 (经典中的经典,必读)
    • 《TCP/IP详解 卷1:协议》
  • 在线文档:
    • man 手册页 (在 Linux 终端输入 man 2 socket, man 2 bind 等)
    • Beej's Guide to Network Programming (非常友好的在线教程,有中文版)
  • 实践:
    • 尝试实现一个 UDP 的回显服务器/客户端。
    • 尝试用 select() 改造服务器,使其能同时处理多个客户端。
    • 实现一个简单的文件传输程序。
-- 展开阅读全文 --
头像
织梦系统登录后我的网站进不去怎么办?
« 上一篇 昨天
织梦责任编辑修改入口在哪?
下一篇 » 昨天

相关文章

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

目录[+]