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

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

Socket(套接字)是操作系统提供的一种用于网络通信的API,它就像一个“电话插座”,程序可以通过它来发送和接收数据,实现不同计算机之间的通信。

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

我们将分为以下几个部分来讲解:

  1. 核心概念:理解Socket的基本原理。
  2. 基本流程:一个简单的客户端/服务器通信流程。
  3. 关键API函数:创建、连接、监听、接受、读写、关闭等。
  4. 一个完整的示例:一个简单的Echo Server(回显服务器)和客户端。
  5. 进阶话题:多线程/多进程处理、select模型等。

核心概念

Socket是什么?

Socket是网络编程的“句柄”(Handle),当你创建一个Socket时,操作系统会返回一个整数,这个整数就是你用来进行后续网络操作的标识符。

两种主要的Socket类型:

  1. 流式套接字 (SOCK_STREAM)
    • 协议:使用 TCP (Transmission Control Protocol)。
    • 特点:面向连接、可靠、有序、不丢包,数据像一个字节流一样传输,适用于要求高可靠性的场景,如文件传输、网页浏览(HTTP/HTTPS)。
  2. 数据报套接字 (SOCK_DGRAM)
    • 协议:使用 UDP (User Datagram Protocol)。
    • 特点:无连接、不可靠、可能丢包、可能乱序,数据以独立的“数据报”形式发送,适用于对速度要求高、能容忍少量丢包的场景,如视频会议、在线游戏、DNS查询。

网络字节序

计算机内部存储数据有两种方式:大端序(高位字节在前)和小端序(低位字节在前),不同的CPU架构可能使用不同的字节序,为了保证网络通信中数据的一致性,规定网络传输必须使用大端序(也叫网络字节序)。

当你需要将一个32位的整数从主机字节序转换为网络字节序时,可以使用 htonl() (Host to Network Long),反之,用 ntohl(),对于16位整数,使用 htons()ntohs()

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

基本流程

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

  1. 创建套接字:调用 socket() 函数,创建一个通信端点。
  2. 绑定地址和端口:调用 bind() 函数,将套接字与一个特定的IP地址和端口号绑定,这样客户端才能知道连接到哪里。
  3. 监听连接:调用 listen() 函数,将套接字设置为“监听”状态,等待客户端的连接请求。
  4. 接受连接:调用 accept() 函数,阻塞等待客户端的连接,当一个客户端连接成功时,accept() 会返回一个新的套接字,专门用于与这个客户端通信,而原来的监听套接字继续等待新的连接。
  5. 收发数据:使用 send() / write()recv() / read() 函数,与客户端进行数据交换。
  6. 关闭套接字:通信结束后,关闭用于通信的套接字和监听套接字。

客户端流程 (以TCP为例)

  1. 创建套接字:同样调用 socket() 函数。
  2. 连接服务器:调用 connect() 函数,向服务器的IP地址和端口号发起连接请求。
  3. 收发数据:连接成功后,使用 send() / write()recv() / read() 函数与服务器通信。
  4. 关闭套接字:通信结束后,关闭套接字。

关键API函数 (Linux/Unix风格)

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

#include <sys/socket.h>   // 核心Socket函数
#include <netinet/in.h>   // IPv4地址结构
#include <arpa/inet.h>    // IP地址转换函数 (如inet_addr)
#include <unistd.h>       // close() 函数
#include <string.h>      // memset(), bzero() 函数
函数 功能 参数说明
socket() 创建一个Socket domain (地址族, 如AF_INET), type (类型, 如SOCK_STREAM), protocol (协议, 如0)
bind() 绑定IP地址和端口 sockfd (Socket描述符), addr (指向sockaddr结构体的指针), addrlen (地址长度)
listen() 开始监听连接 sockfd (Socket描述符), backlog (等待连接的最大队列长度)
accept() 接受一个新连接 sockfd (监听Socket描述符), addr (客户端地址), addrlen (地址长度指针)
connect() 向服务器发起连接 sockfd (Socket描述符), addr (服务器地址), addrlen (地址长度)
send() / write() 通过Socket发送数据 sockfd (Socket描述符), buf (数据缓冲区), len (数据长度), flags (通常为0)
recv() / read() 从Socket接收数据 sockfd (Socket描述符), buf (数据缓冲区), len (缓冲区大小), flags (通常为0)
close() 关闭一个Socket描述符 sockfd (要关闭的Socket描述符)

重要结构体:struct sockaddr_in 这是用于IPv4地址的结构体,比通用的struct sockaddr更方便使用。

struct sockaddr_in {
    short            sin_family;   // 地址族, AF_INET
    unsigned short   sin_port;     // 端口号, 必须用htons()转换
    struct in_addr  sin_addr;     // IP地址
    char             sin_zero[8]; // 填充字节, 使其与struct sockaddr大小一致
};
// struct in_addr 结构体
struct in_addr {
    in_addr_t s_addr; // 32位IP地址, 必须用inet_addr()或inet_pton()转换
};

一个完整的示例

下面是一个简单的TCP Echo Server和Client,客户端发送任何消息,服务器都会原样返回。

服务器端代码 (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 (AF_INET for IPv4, SOCK_STREAM for TCP)
    if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == 0) {
        perror("socket failed");
        exit(EXIT_FAILURE);
    }
    // 设置Socket选项,允许地址重用
    if (setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt))) {
        perror("setsockopt");
        exit(EXIT_FAILURE);
    }
    // 2. 绑定地址和端口
    address.sin_family = AF_INET;
    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);
    }
    // 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 with IP: %s and Port: %d\n",
           inet_ntoa(address.sin_addr), ntohs(address.sin_port));
    // 5. 收发数据
    int valread;
    while (1) {
        valread = read(new_socket, buffer, BUFFER_SIZE);
        if (valread <= 0) {
            // 如果read返回0,表示客户端断开连接;如果返回-1,表示出错
            printf("Client disconnected.\n");
            break;
        }
        printf("Received from 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. 创建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 response: %s\n", buffer);
    // 5. 关闭套接字
    close(sock);
    return 0;
}

如何编译和运行

  1. 编译: 在Linux或macOS终端中,使用gcc进行编译,需要链接-pthread,因为一些系统可能需要它。

    c语言 socket
    (图片来源网络,侵删)
    # 编译服务器
    gcc server.c -o server
    # 编译客户端
    gcc client.c -o client
  2. 运行

    • 先运行服务器

      ./server

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

    • 再运行客户端(可以在另一个终端窗口中):

      ./client

      客户端会连接到服务器,发送 "Hello from client",并打印出服务器的回显 "Hello from client"。

    • 服务器端会打印出客户端的连接信息和接收到的消息。


进阶话题

多客户端处理

上面的服务器一次只能处理一个客户端,当accept()阻塞时,它无法处理新的连接,要处理多个客户端,常见的方法有:

  1. 多进程:父进程调用accept()后,fork()一个子进程来处理与客户端的通信,父进程继续监听。
  2. 多线程:主线程调用accept(),然后创建一个新的子线程来处理客户端通信,主线程继续监听。
  3. I/O多路复用:使用selectpollepoll等系统调用来监视多个Socket的状态,当某个Socket准备好读或写时,select会返回,程序再对它进行操作。epoll是Linux下最高效的I/O多路复用模型。

错误处理

在实际应用中,所有socket函数都应该进行错误检查。perror()是一个非常有用的函数,它会打印出当前错误信息。

Windows平台

如果你需要在Windows上运行,需要做以下调整:

  1. 包含不同的头文件:#include <winsock2.h>#include <ws2tcpip.h>
  2. 在程序开始时,调用 WSAStartup() 初始化Winsock库。
  3. 在程序结束时,调用 WSACleanup() 清理Winsock库。
  4. close() 函数在Windows上是 closesocket()
  5. 编译时需要链接 ws2_32.lib 库。

希望这份详细的指南能帮助你理解C语言中的Socket编程!

-- 展开阅读全文 --
头像
C语言return语句如何正确使用?
« 上一篇 04-13
dede上下篇定义规则是什么?
下一篇 » 04-13

相关文章

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

目录[+]