网络编程

字节序

字节序是计算机中多字节数据存储或传输时各字节的排列顺序。其核心区别在于高位字节与低位字节在内存中的存放顺序,主要分为大端序和小端序两种类型
1. Little endian:将低序字节存储在起始地址
2. Big endian:将高序字节存储在起始地址
image.png

image.png

socket编程步骤

image.png

工作流程简述

创建一个 socket 仅仅是拿到了一个“门户”,要真正开始通信,还需要根据你是客户端还是服务端进行后续操作:

客户端典型流程

  1. socket() – 创建 socket。
  2. connect() – 主动连接到服务器。
  3. send() / recv() – 与服务器进行数据交换。
  4. close() – 关闭连接。

服务端典型流程

  1. socket() – 创建 socket。
  2. bind() – 将 socket 绑定到一个本地 IP 地址和端口号。
  3. listen() – 监听来自客户端的连接请求(仅用于 TCP)。
  4. accept() – 接受一个连接,得到一个新的用于通信的 socket。
  5. send() / recv() – 与连接的客户端进行数据交换。
  6. close() – 关闭连接。

socket函数 创建 socket

socket 函数是网络编程中最核心、最基础的函数,它是创建网络通信端点的起点。无论是像浏览器这样的客户端,还是像Web服务器这样的服务端,一切网络通信都始于调用 socket 函数。

核心概念

你可以把 socket 想象成 “网络通信的端点” 或者 “网络数据交换的门户”。应用程序要通过网络发送或接收数据,必须首先创建一个 socket。这个 socket 在操作系统中被标识为一个组合,通常包括:

  • IP 地址:标识网络中的一台主机。
  • 端口号:标识主机上的一个特定应用程序。
  • 协议类型:定义如何传输数据(如 TCP 或 UDP)。
    socket 函数的作用就是向操作系统申请创建这样一个通信端点,并返回一个标识符(文件描述符)以供后续操作使用。

函数原型(C语言)

通常在头文件 <sys/socket.h> 中定义。

#include <sys/socket.h>
int socket(int domain, int type, int protocol);

参数解析

这个函数有三个参数,它们共同决定了创建的 socket 所具有的特性:

  1. domain (协议族/地址族)
    • 指定 socket 将使用哪种地址体系进行通信。
    • 常见值:
      • AF_INET: IPv4 网络协议,这是最常用的家族,使用 32 位 IP 地址(如 192.168.1.1)。
      • AF_INET6: IPv6 网络协议,使用 128 位 IP 地址。
      • AF_UNIX 或 AF_LOCAL: 本地通信,用于同一台机器上的进程间通信,使用文件路径名作为地址。
  2. type ( socket 类型)
    • 指定了 socket 的通信语义,即数据传输的方式。
    • 常见值:
      • SOCK_STREAM: 提供面向连接的、可靠的、双向字节流通信。它使用 TCP 协议。 characteristics: 数据无丢失、无重复、按序到达。就像打电话,需要先建立连接,通信可靠。
      • SOCK_DGRAM: 提供无连接的、不可靠的数据报通信。它使用 UDP 协议。 characteristics: 尽力传输,可能丢失或乱序,但开销小,速度快。就像发短信,无需连接,直接发送。
      • SOCK_RAW: 原始套接字,允许对底层协议(如 IP 或 ICMP)进行直接访问,可以手动构造协议头。常用于网络探测或实现自定义协议。
  3. protocol (协议)
    • 通常设置为 0,表示由系统根据 domain 和 type 的组合自动选择默认的协议。
    • 例如:
      • (AF_INET, SOCK_STREAM, 0) 会自动选择 TCP 协议。
      • (AF_INET, SOCK_DGRAM, 0) 会自动选择 UDP 协议。
    • 只有在需要某种特定协议时(如在原始套接字中指定 ICMP),才需要显式指定协议号(如 IPPROTO_ICMP)。

返回值

  • 成功: 返回一个新的** socket 文件描述符**(一个非负整数)。后续的所有操作(如连接、发送、接收、关闭)都将使用这个描述符来引用这个 socket。
  • 失败: 返回 -1,并设置全局变量 errno 以指示错误类型(如 EACCES 权限不足、EMFILE 进程打开文件数达到上限等)。
创建一个使用 TCP 协议的 IPv4 socket
#include <sys/socket.h>
#include <stdio.h>
#include <stdlib.h>

int main() {
    int sockfd;

    // 创建 socket
    // AF_INET: IPv4
    // SOCK_STREAM: TCP
    // 0: 自动选择协议
    sockfd = socket(AF_INET, SOCK_STREAM, 0);

    if (sockfd == -1) {
        perror("Socket creation failed");
        exit(EXIT_FAILURE);
    }

    printf("Socket created successfully. File descriptor: %d\n", sockfd);

    // ... 这里可以进行 bind, connect, listen 等后续操作 ...

    // 最后不要忘记关闭 socket
    close(sockfd);
    return 0;
}
创建一个使用 UDP 协议的 IPv4 socket
// 只需将 type 参数改为 SOCK_DGRAM
sockfd = socket(AF_INET, SOCK_DGRAM, 0);

inet_aton函数 将点分十进制格式的IPv4地址字符串

inet_aton(Address TO Number)是一个用于将点分十进制格式的IPv4地址字符串(如 “192.168.1.1”)转换为网络字节序的32位二进制整数的函数。这个二进制形式可以直接用于 struct sockaddr_in 的 sin_addr.s_addr 字段。

函数原型

在头文件 <arpa/inet.h> 中声明。

#include <arpa/inet.h>
int inet_aton(const char *cp, struct in_addr *inp);

参数解析

  1. const char *cp
    • 这是一个输入参数,指向一个以空字符结尾的字符串,该字符串包含要转换的点分十进制IPv4地址。
    • 例如:"192.168.142.128""127.0.0.1" 或 "8.8.8.8"
  2. struct in_addr *inp
    • 这是一个输出参数,是一个指向 struct in_addr 的指针。函数会将转换后的32位网络字节序的IPv4地址存储到这个结构体的 s_addr 字段中。
    • struct in_addr 通常定义如下:
struct in_addr {
    in_addr_t s_addr; // 32位的IPv4地址(网络字节序)
};

返回值

  • 成功: 返回 1(非零值)。
  • 失败(如果输入的字符串不是有效的IPv4地址): 返回 0
    注意: 与许多C库函数不同,inet_aton 成功时返回1,失败返回0,而不是返回-1并设置errno。

功能详解

inet_aton 完成了一个关键任务:地址表示形式的转换

  • 人类可读形式 -> 机器可读形式
    • 输入:"192.168.142.128" (字符串)
    • 输出:0x808ea8c0 (32位整数,网络字节序)
      网络字节序: 转换后的整数是大端模式(Big-endian),这是TCP/IP协议栈要求的标准字节序。这意味着最重要的字节(最高位字节)存储在最低的内存地址上。

inet_ntoa函数 将网络格式的ip地址转为字符串格式

函数概述

inet_ntoa(Network TO Address)是一个用于将网络字节序的32位二进制IPv4地址转换回点分十进制格式字符串的函数。它执行的是 inet_aton 的逆操作。

函数原型

在头文件 <arpa/inet.h> 中声明。

#include <arpa/inet.h>
char *inet_ntoa(struct in_addr in);

参数解析

  • struct in_addr in
    • 这是一个输入参数,是一个 struct in_addr 结构体(而不是指针),其中包含要转换的32位网络字节序IPv4地址。
    • 结构体定义通常为:
struct in_addr {
    in_addr_t s_addr; // 32位的IPv4地址(网络字节序)
};

返回值

  • 返回一个指向静态分配缓冲区的指针,该缓冲区中包含点分十进制格式的IPv4地址字符串。
  • 这个函数不需要手动释放内存,但也不应该长期保存返回的指针。

功能详解

inet_ntoa 完成了一个关键任务:二进制地址到人类可读形式的转换

  • 机器可读形式 -> 人类可读形式
    • 输入:0x808ea8c0 (32位整数,网络字节序)
    • 输出:"192.168.142.128" (字符串)

重要特性: 函数会自动处理网络字节序到主机字节序的转换,你不需要先调用 ntohl


bind函数 和socket关联起来

bind 函数是网络编程中服务器端程序至关重要的一步。它的核心作用是将一个 socket 与一个特定的本地网络地址(IP 地址 + 端口号)相关联

你可以这样理解:

  • socket() 函数就像是买了一部手机。
  • bind() 函数就像是给这部手机插上一张 SIM 卡并分配一个手机号码
    • 如果没有手机号(bind),别人就无法通过一个固定的号码联系到你。
  • 对于服务器来说,它必须有一个固定且众所周知的地址,这样客户端才知道要连接到哪里。bind 就是完成这个“公布地址”动作的函数。

函数原型(C语言)

通常在头文件 <sys/socket.h> 中定义。

#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

参数解析

  1. int sockfd
    • 这是 socket() 函数成功调用后返回的 socket 文件描述符bind 操作就是要给这个特定的 socket 分配地址。
  2. const struct sockaddr *addr
    • 这是一个指向通用地址结构体的指针。它指向一个包含了你要绑定到的 IP 地址和端口号的结构体。
    • 这是一个通用类型。在实际使用时,我们需要传入特定协议族的地址结构(如 struct sockaddr_in for IPv4),但在函数调用时,必须强制转换为 struct sockaddr * 类型。
  3. socklen_t addrlen
    • 这是第二个参数 addr 所指向的结构体的长度(以字节为单位)。通常使用 sizeof(struct sockaddr_in) 来获取。

返回值

  • 成功: 返回 0
  • 失败: 返回 -1,并设置全局变量 errno 以指示错误类型。最常见的错误是 EADDRINUSE(指定的地址和端口已被占用)。

核心工作:填充地址结构体 sockaddr_in

对于最常用的 IPv4 (AF_INET),我们使用 struct sockaddr_in 来存储地址信息。在调用 bind 之前,必须正确地填充这个结构体。

struct sockaddr_in 结构(定义在 <netinet/in.h>

struct sockaddr_in {
    sa_family_t    sin_family;   // 地址族: AF_INET (IPv4)
    in_port_t      sin_port;     // 网络字节序的端口号
    struct in_addr sin_addr;     // IPv4 地址结构
    char           sin_zero[8];  // 填充字段,通常设置为0
};

struct in_addr {
    uint32_t       s_addr;       // 网络字节序的IPv4地址
};

填充这个结构体的关键步骤:

  1. 设置地址族 (sin_family)
    • 直接设置为 AF_INET
  2. 设置端口号 (sin_port)
    • 端口号必须是网络字节序(大端模式)。我们通常使用 htons() (Host TO Network Short) 函数将主机字节序的端口号(一个16位的短整型)进行转换。
    • 例如:my_addr.sin_port = htons(8080); // 绑定到8080端口
  3. 设置 IP 地址 (sin_addr.s_addr)
    • IP 地址也必须是网络字节序。我们可以使用:
      • INADDR_ANY:这是一个特殊的常量(通常是 0.0.0.0),表示绑定到机器上所有可用的网络接口。服务器通常这样设置,这样无论客户端通过哪个网卡(如以太网、Wi-Fi)连过来,服务器都能接收到连接。这是最常见的用法。
      • inet_addr("192.168.1.100"):将点分十进制的IP字符串转换为网络字节序的32位整数。
      • 直接指定一个特定的IP地址(需要先转换)。
    • 例如:my_addr.sin_addr.s_addr = htonl(INADDR_ANY); // 绑定到所有本地IP

示例

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h> // 包含 sockaddr_in 结构

int main() {
    int server_fd;
    struct sockaddr_in server_addr;

    // 1. 创建 socket
    server_fd = socket(AF_INET, SOCK_STREAM, 0);
    if (server_fd == -1) {
        perror("socket failed");
        exit(EXIT_FAILURE);
    }

    // 2. 准备地址结构体
    memset(&server_addr, 0, sizeof(server_addr)); // 清空结构体
    server_addr.sin_family = AF_INET; // IPv4
    server_addr.sin_addr.s_addr = htonl(INADDR_ANY); // 绑定到所有本地IP
    server_addr.sin_port = htons(8080); // 绑定到8080端口

    // 3. 调用 bind
    if (bind(server_fd, (struct sockaddr *)&server_addr, sizeof(server_addr)) == -1) {
        perror("bind failed");
        close(server_fd);
        exit(EXIT_FAILURE);
    }

    printf("Server successfully bound to port 8080 on all interfaces.\n");

    // ... 后续可以调用 listen, accept 等 ...

    close(server_fd);
    return 0;
}

listen函数 监听

listen 函数是TCP服务器端编程中一个承上启下的关键步骤。它的作用非常明确:将一个已绑定的 socket 从“主动”转换为“被动”模式,并通知操作系统开始监听来自客户端的连接请求。

核心概念与类比

一个完美的类比是 “电话总机”

  1. socket():你买了一部电话机。
  2. bind():你给这部电话机申请了一个固定的电话号码(如 400-123-4567)
  3. listen():你打开电话机的响铃功能并插上电源,告诉系统和外界:“我的这个号码已经准备就绪,可以接听来电了”。同时,你设置了一个“呼叫等待队列”的长度,决定最多能有多少个电话同时打进来等待接听。
  4. accept():当电话铃响时,你拿起听筒,与对方建立真正的通话连接。

如果没有调用 listen,即使你的 socket 已经绑定了地址,它也会对 incoming 的连接请求置之不理,操作系统会直接拒绝(RST)客户端的连接尝试。

函数原型(C语言)

在头文件 <sys/socket.h> 中定义。

#include <sys/socket.h>
int listen(int sockfd, int backlog);

参数解析

  1. int sockfd
    • 这是 socket() 函数创建并由 bind() 函数绑定了地址的 socket 文件描述符。这个 socket 必须是 SOCK_STREAM 类型(即TCP socket)。
  2. int backlog
    • 这是整个函数中最关键且容易误解的参数。它定义了该 socket 的未完成连接队列已完成连接队列总和的最大长度
    • 含义: 当多个客户端同时发起连接时,TCP的三次握手需要时间来完成。backlog 参数限制了在这些握手过程处于不同阶段时,系统能为该 socket 排队等待处理的连接请求的最大数量
    • 队列详解
      • 未完成连接队列 (SYN队列): 存放已收到客户端SYN包、正在等待完成三次握手的连接。状态为 SYN_RCVD
      • 已完成连接队列 (Accept队列): 存放已经完成三次握手、等待服务器调用 accept() 来取走的连接。状态为 ESTABLISHED
    • 如何设置
      • 历史上,这个参数的含义在不同系统中变化很大。现代Linux系统通常将其定义为 已完成连接队列 (Accept队列) 的最大长度
      • 这是一个建议值,内核会根据实际情况进行调整(例如,在 /proc/sys/net/core/somaxconn 中定义了系统级别的全局最大值)。
      • 对于高并发服务器,通常会设置为一个较大的值(如 128, 1024 或更多),但不应超过系统的 somaxconn 限制。
      • 如果队列已满时又有新的连接完成握手,服务器可能会忽略客户端发来的ACK包,导致客户端认为连接已建立,而服务器却将其丢弃。

返回值

  • 成功: 返回 0
  • 失败: 返回 -1,并设置全局变量 errno 以指示错误类型。
    • 常见的错误:EBADF(无效的文件描述符)、EINVAL(socket未绑定或已连接)、ENOTSOCK(文件描述符不是socket)。

简单示例

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>

#define PORT 8080
#define BACKLOG 10 // 定义等待队列的最大长度

int main() {
    int server_fd;
    struct sockaddr_in server_addr;

    // 1. 创建 socket
    server_fd = socket(AF_INET, SOCK_STREAM, 0);
    if (server_fd == -1) {
        perror("socket failed");
        exit(EXIT_FAILURE);
    }

    // 2. 绑定地址
    server_addr.sin_family = AF_INET;
    server_addr.sin_addr.s_addr = INADDR_ANY; // 监听所有本地网卡
    server_addr.sin_port = htons(PORT);

    if (bind(server_fd, (struct sockaddr*)&server_addr, sizeof(server_addr)) < 0) {
        perror("bind failed");
        close(server_fd);
        exit(EXIT_FAILURE);
    }

    // 3. 开始监听!
    if (listen(server_fd, BACKLOG) < 0) {
        perror("listen failed");
        close(server_fd);
        exit(EXIT_FAILURE);
    }

    printf("Server is now listening on port %d...\n", PORT);

    // ... 后续会进入循环,调用 accept() 来接受连接 ...
    // int client_fd = accept(server_fd, (struct sockaddr*)&client_addr, &addrlen);

    close(server_fd);
    return 0;
}

accept函数 拿起听筒,并创建一个通话通道

accept 函数的作用是:从已完成连接队列(Accept Queue)中取出第一个连接请求,为这个连接创建一个新的socket,并返回其文件描述符。

核心概念与完美类比

继续使用“电话总机”的类比:

  1. socket(): 买一部电话机。
  2. bind(): 申请一个电话号码(INADDR_ANY:8080)。
  3. listen()打开电话响铃,并设置呼叫等待队列容量(backlog。现在电话可以响了。
  4. accept()电话铃响了!你拿起听筒(accept。这个动作:
    • 返回一个新的通话通道:你手里拿着的听筒(返回的新socket)是专门用于和这位来电者对话的。
    • 总机保持空闲:你原来的那部主机(监听socket)被释放出来,立刻放回桌上,继续等待下一个来电。它永远不会用于直接通话,只负责接听新的呼叫。
      这个类比精确地解释了为什么需要两个socket:
  • 监听Socket (Listening Socket): 由 socket() 创建,bind() 和 listen() 设置。它就像一个总机接线员,只负责接受新的呼叫请求,不进行实际的数据通信
  • 已连接Socket (Connected Socket): 由 accept() 返回。它代表一个已经建立好的、唯一的网络连接,用于与特定的客户端进行 send() 和 recv() 操作。

函数原型(C语言)

在头文件 <sys/socket.h> 中定义。

#include <sys/socket.h>
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

参数解析

  1. int sockfd
    • 这是 listen() 函数正在监听的监听socket的文件描述符。这个socket必须已经成功调用了 bind() 和 listen()
  2. struct sockaddr *addr
    • 这是一个输出参数。它是一个指向缓冲区的指针,accept 函数会在其中填充发起连接请求的那个客户端的地址信息(IP地址和端口号)。
    • 如果你不关心客户端的地址,可以简单地设置为 NULL
  3. socklen_t *addrlen
    • 这是一个输入输出参数
    • 输入时:它是一个指针,指向一个整数,该整数表示你提供的 addr 缓冲区的长度。
    • 输出时accept 函数会修改它指向的值,将其设置为实际写入 addr 中的地址信息的真实长度。
    • 如果 addr 为 NULL,那么这个参数也应该是 NULL

返回值

  • 成功: 返回一个新的、非负的文件描述符。这个新的socket描述符就是已连接socket,用于与刚刚接受的客户端进行通信。
  • 失败: 返回 -1,并设置全局变量 errno 以指示错误类型(例如 EBADFEINVALECONNABORTED)。

工作流程与行为

  1. 阻塞行为: 默认情况下,如果已完成连接队列为空(即没有客户端连接上来),accept() 函数会阻塞(使程序暂停执行),直到有新的连接到达。
  2. 取出连接: 一旦有连接可用,accept 就从队列中取出第一个连接。
  3. 创建新Socket: 系统为这个连接创建一个全新的socket。这个新socket与监听socket的地址族(AF_INET)和类型(SOCK_STREAM)相同,但它已经与一个特定的远程客户端peer关联好了。
  4. 返回并通信: 函数返回新socket的文件描述符。服务器随后使用这个新描述符与客户端进行读写操作。

关键点: 调用 accept() 并不会影响原始的监听socket sockfd。该监听socket仍然保持打开状态,可以继续用于下一次 accept() 调用来接受新的连接。这就是服务器能够同时处理多个客户端的基础。

示例

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h> // 用于 inet_ntoa

#define PORT 8080
#define BACKLOG 5

int main() {
    int server_fd, client_fd;
    struct sockaddr_in server_addr, client_addr;
    socklen_t client_addr_len = sizeof(client_addr);
    char client_ip[INET_ADDRSTRLEN];

    // 1. 创建、绑定、监听 socket (步骤1-3)
    server_fd = socket(AF_INET, SOCK_STREAM, 0);
    // ... 错误检查 ...

    server_addr.sin_family = AF_INET;
    server_addr.sin_addr.s_addr = INADDR_ANY;
    server_addr.sin_port = htons(PORT);

    if (bind(server_fd, (struct sockaddr*)&server_addr, sizeof(server_addr)) < 0) {
        perror("bind failed");
        exit(EXIT_FAILURE);
    }

    if (listen(server_fd, BACKLOG) < 0) {
        perror("listen failed");
        exit(EXIT_FAILURE);
    }

    printf("Server listening on port %d...\n", PORT);

    // 2. 主循环:等待并接受连接
    while(1) {
        // accept() 会阻塞,直到有客户端连接
        printf("Waiting for a connection...\n");
        client_fd = accept(server_fd, (struct sockaddr*)&client_addr, &client_addr_len);
        if (client_fd < 0) {
            perror("accept failed");
            continue; // 不要退出,继续接受下一个连接
        }

        // 将客户端的IP地址从网络字节序转换为可读字符串
        inet_ntop(AF_INET, &(client_addr.sin_addr), client_ip, INET_ADDRSTRLEN);
        printf("Connection accepted from %s:%d\n", client_ip, ntohs(client_addr.sin_port));

        // 3. 现在可以使用 client_fd 与客户端通信!
        // 例如:send(client_fd, "Hello, Client!", 14, 0);
        // 通常这里会fork()一个子进程或创建一个新线程来处理这个客户端,
        // 这样主线程就可以立刻回到 accept() 等待下一个客户端。

        // 4. 通信完毕,关闭这个客户端的连接socket。
        // (注意:不能关闭 server_fd!)
        close(client_fd);
        printf("Connection with %s closed.\n", client_ip);
    }

    // 通常不会执行到这里,但如果需要退出,应关闭 server_fd
    close(server_fd);
    return 0;
}

connect函数  主动与服务器建立连接

connect 函数是 TCP客户端 编程中的核心函数,它的作用是主动与服务器建立连接。对于TCP协议,它会触发TCP三次握手过程;对于UDP协议,它则只是设置默认的目标地址。

函数原型(C语言)

在头文件 <sys/socket.h> 中定义。

#include <sys/socket.h>
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

参数解析

  1. int sockfd
    • 这是通过 socket() 函数创建的客户端socket的文件描述符
    • 这个socket必须是未连接的,并且通常是 SOCK_STREAM (TCP) 或 SOCK_DGRAM (UDP) 类型。
  2. const struct sockaddr *addr
    • 这是一个指向服务器地址结构体的指针,包含了客户端想要连接的服务器的IP地址和端口号
    • 与 bind() 和 accept() 类似,这里需要传入特定协议族的地址结构(如 struct sockaddr_in for IPv4),但在函数调用时必须强制转换为 struct sockaddr * 类型。
  3. socklen_t addrlen
    • 这是第二个参数 addr 所指向的结构体的长度(以字节为单位)。
    • 通常使用 sizeof(struct sockaddr_in)

返回值

  • 成功: 返回 0
  • 失败: 返回 -1,并设置全局变量 errno 以指示错误类型。

功能详解

connect 函数的行为根据socket类型的不同而有所不同:

1. 对于TCP socket (SOCK_STREAM)

  • 触发三次握手: connect 会主动发起TCP三次握手过程,与指定的服务器建立可靠的连接。
  • 阻塞行为: 默认情况下,connect 会阻塞,直到连接成功建立或发生错误。
  • 连接成功: 只有三次握手完成后,connect 才会返回成功。此后,客户端可以使用 send() 和 recv() 与服务器进行通信。

2. 对于UDP socket (SOCK_DGRAM)

  • 不建立实际连接: UDP是无连接的,所以 connect 不会触发任何网络握手过程。
  • 设置默认目标: 它只是在内核中记录服务器的地址信息,设置为后续 send() 和 recv() 调用的默认目标。
  • 作用
    • 之后可以直接使用 send() 而不是 sendto()(因为目标地址已已知)
    • 可以使用 recv() 而不是 recvfrom()(但只会接收来自该地址的数据报)
    • 提高效率:避免每次发送数据时重复指定目标地址

gets 函数 标准输入

函数概述

gets 函数曾经是C标准库(<stdio.h>)中的一个函数,用于从标准输入(stdin) 读取一行字符串,直到遇到换行符或文件结束符。

函数原型

char *gets(char *str);

参数解析

  • char *str
    • 这是一个指向字符数组(缓冲区)的指针,用于存储读取的字符串。
    • 缓冲区应该足够大,以容纳用户可能输入的任何内容。

返回值

  • 成功: 返回传入的 str 指针。
  • 失败或到达文件末尾: 返回 NULL

sprintf函数 格式化数据写入字符串

sprintf 是 C 语言中一个非常重要且强大的函数,用于将格式化数据写入字符串。它的名称代表 “String PRINT Formatted”。

函数原型

在头文件 <stdio.h> 中声明。

#include <stdio.h>
int sprintf(char *str, const char *format, ...);

参数解析

  1. char *str
    • 这是一个指向目标缓冲区的指针,格式化后的字符串将存储在这里。
    • 非常重要:你必须确保这个缓冲区足够大,能够容纳格式化后的所有内容,包括结尾的空字符 \0
  2. const char *format
    • 这是一个格式化字符串,包含要写入的文本和格式说明符(以 % 开头的特殊标记)。
    • 格式说明符指定了后续参数如何被格式化并插入到字符串中。
  3. ... (可变参数)
    • 这是零个或多个附加参数,这些参数的值将根据格式字符串中的格式说明符被格式化和插入。

返回值

  • 成功:返回写入目标字符串的字符数量(不包括结尾的空字符)。
  • 失败:返回一个负数。

格式说明符

sprintf 支持丰富的格式说明符,以下是一些常用的:

格式说明符描述示例
%d 或 %i有符号十进制整数sprintf(buf, "%d", 42) → "42"
%u无符号十进制整数sprintf(buf, "%u", 42) → "42"
%f浮点数sprintf(buf, "%f", 3.14) → "3.140000"
%c字符sprintf(buf, "%c", 'A') → "A"
%s字符串sprintf(buf, "%s", "hello") → "hello"
%x十六进制整数(小写)sprintf(buf, "%x", 255) → "ff"
%X十六进制整数(大写)sprintf(buf, "%X", 255) → "FF"
%o八进制整数sprintf(buf, "%o", 8) → "10"
%p指针地址sprintf(buf, "%p", &var) → "0x7ffd42a"
%%百分号字符本身sprintf(buf, "%%") → "%"
格式说明符可以包含修饰符来控制输出
  • %10s – 最小宽度为10字符,右对齐
  • %-10s – 最小宽度为10字符,左对齐
  • %.2f – 浮点数保留2位小数
  • %5.2f – 总宽度5字符,保留2位小数

服务端代码

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h> // 用于 inet_ntoa

int main()
{
    int s_fd;//socket返回的标识符

    s_fd = socket(AF_INET, SOCK_STREAM, 0);//创建 socket,以IPV4,TCP协议创建
    if(s_fd == -1)
    {
        perror("socket");
        exit(-1);
    }

    struct sockaddr_in s_addr;//关联socket的结构体变量
    s_addr.sin_family = AF_INET;//设为IPV4
    s_addr.sin_port = htons(8989);//字节序的端口号,htons绑定端口
    inet_aton("127.0.0.1", &s_addr.sin_addr);//将点分十进制的IP字符串转换为网络字节序的32位整数。

    bind(s_fd, (struct sockaddr *)&s_addr, sizeof(s_addr));//socket标识符,关联socket的结构体变量地址,结构体大小

    listen(s_fd, 10);//监听(socket标识符,连接队列长度)

    int c_fd = accept(s_fd, NULL, NULL);//连接请求接通(socket标识符,,)

    printf("connect\n");//接通了就打印

    while(1);

    return 0;
}
image.png

有读写功能

#include <stdio.h>      // 标准输入输出函数,如printf, perror
#include <stdlib.h>     // 标准库函数,如exit
#include <string.h>     // 字符串处理函数,如memset
#include <unistd.h>     // Unix标准函数,如read, write, close
#include <sys/socket.h> // 套接字相关函数和数据结构
#include <netinet/in.h> // Internet地址族相关定义,如sockaddr_in
#include <arpa/inet.h>  // IP地址转换函数,如inet_aton, inet_ntoa

int main()
{
    int s_fd; // 服务器监听socket的文件描述符
    int n_read; // 读取到的字节数
    char readBuf[128]; // 读取数据的缓冲区

    char *msg = "I get your connect"; // 要发送给客户端的消息
    struct sockaddr_in s_addr; // 服务器地址结构体
    struct sockaddr_in c_addr; // 客户端地址结构体

    // 清空地址结构体,确保没有垃圾数据
    memset(&s_addr, 0, sizeof(struct sockaddr_in));
    memset(&c_addr, 0, sizeof(struct sockaddr_in));

    // 创建socket,AF_INET表示IPv4,SOCK_STREAM表示TCP协议,0表示使用默认协议
    s_fd = socket(AF_INET, SOCK_STREAM, 0);
    if(s_fd == -1) // 检查socket创建是否成功
    {
        perror("socket"); // 打印错误信息
        exit(-1); // 退出程序
    }

    // 设置服务器地址参数
    s_addr.sin_family = AF_INET; // 地址族设为IPv4
    s_addr.sin_port = htons(8989); // 端口号转换为网络字节序,绑定到8989端口
    // 将点分十进制的IP字符串转换为网络字节序的32位整数,并存入地址结构体
    inet_aton("192.168.142.128", &s_addr.sin_addr);

    // 将socket与服务器地址绑定
    bind(s_fd, (struct sockaddr *)&s_addr, sizeof(struct sockaddr_in));

    // 开始监听连接请求,设置连接队列最大长度为10
    listen(s_fd, 10);

    // 客户端地址结构体长度
    int clen = sizeof(struct sockaddr_in);
    // 接受客户端连接请求,返回用于通信的新socket描述符
    // 同时会填充客户端地址信息到c_addr中
    int c_fd = accept(s_fd, (struct sockaddr *)&c_addr, &clen);
    if(c_fd == -1) // 检查accept是否成功
    {
        perror("accept");
        exit(-1);
    }

    // 打印连接的客户端IP地址(从网络字节序转换为点分十进制字符串)
    printf("connect %s\n", inet_ntoa(c_addr.sin_addr));

    // 从客户端socket读取数据到缓冲区,最多读取128字节
    n_read = read(c_fd, readBuf, sizeof(readBuf));
    if(n_read == -1) // 检查read是否成功
    {
        perror("read");
        exit(-1);
    }
    else
    {
        // 打印读取到的字节数和内容
        printf("get message:%d.%s\n", n_read, readBuf);
    }

    // 向客户端socket写入消息
    write(c_fd, msg, strlen(msg));



    close(c_fd);//关闭socket
    close(s_fd);

    return 0;
}
image.png

socket客户端代码实现

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h> // 用于 inet_ntoa

int main()
{
    int c_fd;//socket返回的标识符
    int n_read;
    char readBuf[128];

    char *msg = "msg from client";
    struct sockaddr_in c_addr;

    memset(&c_addr, 0, sizeof(struct sockaddr_in));

    c_fd = socket(AF_INET, SOCK_STREAM, 0);//创建 socket,以IPV4,TCP协议创建
    if(c_fd == -1)
    {
        perror("socket");
        exit(-1);
    }

    c_addr.sin_family = AF_INET;//设为IPV4
    c_addr.sin_port = htons(8989);//字节序的端口号,htons绑定端口
    inet_aton("192.168.142.128", &c_addr.sin_addr);//将点分十进制的IP字符串转换为网络字节序的32位整数。

    if(connect(c_fd, (struct sockaddr *)&c_addr, sizeof(struct sockaddr_in)) == -1)
    {
        perror("connect");
        exit(-1);
    }

    if(write(c_fd, msg, strlen(msg)) == -1)
    {
        perror("write");
        exit(-1);
    }

    n_read = read(c_fd, readBuf, sizeof(readBuf));
    if(n_read == -1)
    {
        perror("read");
        exit(-1);
    }
    else
    {
        printf("get message from server:%d.%s\n",n_read, readBuf);
    }

    return 0;
}

接收多方客户端信息

server.c

#include <stdio.h>          // 标准输入输出头文件
#include <sys/types.h>      // 提供数据类型定义
#include <sys/socket.h>     // 提供socket函数及相关数据结构
//#include <linux/in.h>     // Linux特定的Internet地址定义(已注释掉)
#include <netinet/in.h>     // 提供sockaddr_in等Internet地址结构
#include <arpa/inet.h>      // 提供IP地址转换函数
#include <stdlib.h>         // 提供exit、atoi等标准库函数
#include <string.h>         // 提供字符串处理函数
//chenlichen (可能是作者姓名或注释标记)

int main(int argc, char **argv)
{
    int s_fd;               // 服务器套接字文件描述符
    int c_fd;               // 客户端连接套接字文件描述符
    int n_read;             // 读取数据的字节数
    char readBuf[128];      // 读取数据的缓冲区

    int mark = 0;           // 客户端计数器
    char msg[128] = {0};    // 发送消息的缓冲区
    //  char *msg = "I get your connect";  // 硬编码消息(已注释)
    struct sockaddr_in s_addr;  // 服务器地址信息结构体
    struct sockaddr_in c_addr;  // 客户端地址信息结构体

    // 检查命令行参数数量是否正确(程序名 + IP地址 + 端口号)
    if(argc != 3){
        printf("param is not good\n");  // 参数不正确时输出提示
        exit(-1);                       // 退出程序
    }

    // 初始化服务器和客户端地址结构体为0
    memset(&s_addr,0,sizeof(struct sockaddr_in));
    memset(&c_addr,0,sizeof(struct sockaddr_in));

    //1. 创建套接字
    s_fd = socket(AF_INET, SOCK_STREAM, 0);  // 创建IPv4 TCP套接字
    if(s_fd == -1){              // 检查套接字创建是否成功
        perror("socket");        // 输出错误信息
        exit(-1);                // 退出程序
    }

    // 配置服务器地址信息
    s_addr.sin_family = AF_INET;                       // 使用IPv4地址族
    s_addr.sin_port = htons(atoi(argv[2]));            // 设置端口号(转换为网络字节序)
    inet_aton(argv[1],&s_addr.sin_addr);               // 将字符串IP地址转换为二进制格式

    //2. 绑定套接字到指定地址和端口
    bind(s_fd,(struct sockaddr *)&s_addr,sizeof(struct sockaddr_in));

    //3. 监听连接请求,设置最大等待连接数为10
    listen(s_fd,10);

    //4. 接受客户端连接
    int clen = sizeof(struct sockaddr_in);  // 客户端地址结构体长度
    while(1){  // 主循环,持续接受客户端连接
        // 接受客户端连接
        c_fd = accept(s_fd,(struct sockaddr *)&c_addr,&clen);
        if(c_fd == -1){          // 检查接受连接是否成功
            perror("accept");    // 输出错误信息
        }

        mark++;  // 增加客户端计数器
        // 打印客户端IP地址
        printf("get connect: %s\n",inet_ntoa(c_addr.sin_addr));

        // 创建子进程处理客户端请求
        if(fork() == 0){  // 子进程
            // 创建孙子进程定期发送欢迎消息
            if(fork()==0){  // 孙子进程
                while(1){   // 循环发送欢迎消息
                    // 格式化欢迎消息
                    sprintf(msg,"welcom No.%d client",mark);
                    // 发送消息到客户端
                    write(c_fd,msg,strlen(msg));
                    sleep(3);  // 等待3秒
                }   
            }   

            //5. 读取客户端数据
            while(1){
                memset(readBuf,0,sizeof(readBuf));   // 清空读取缓冲区
                n_read = read(c_fd, readBuf, 128);   // 从客户端读取数据
                if(n_read == -1){                    // 检查读取是否出错
                    perror("read");                  // 输出读取错误信息
                }else if(n_read>0){                  // 成功读取到数据
                    printf("\nget: %d\n",n_read);    // 输出读取的字节数
                }else{                               // 读取到0字节(客户端断开连接)
                    printf("client quit\n");         // 输出客户端退出信息
                    break;                           // 退出循环
                }
            }
            break;  // 退出子进程
        }
    }
    return 0;  // 程序结束
}

client.c

#include <stdio.h>          // 标准输入输出函数库
#include <sys/types.h>      // 定义数据类型,如size_t、ssize_t等
#include <sys/socket.h>     // 套接字编程接口
//#include <linux/in.h>     // Linux IPv4头文件(已被注释掉)
#include <netinet/in.h>     // Internet地址族结构体定义
#include <arpa/inet.h>      // IP地址转换函数
#include <stdlib.h>         // 标准库函数,如exit、atoi等
#include <string.h>         // 字符串处理函数

int main(int argc, char **argv)
{
    int c_fd;               // 客户端套接字文件描述符
    int n_read;             // 读取数据的字节数
    char readBuf[128];      // 用于存储从服务器读取的数据的缓冲区
    int tmp;                // 未使用的临时变量

    //  char *msg = "msg from client";  // 硬编码消息(已被注释掉)
    char msg[128] = {0};    // 用于存储用户输入的消息的缓冲区
    struct sockaddr_in c_addr;  // 服务器地址信息结构体

    memset(&c_addr,0,sizeof(struct sockaddr_in));  // 将地址结构体清零

    // 检查命令行参数数量是否正确
    if(argc != 3){
        printf("param is not good\n");  // 参数不正确时输出提示
        exit(-1);                       // 退出程序
    }

    printf("%d\n",getpid());  // 打印当前进程ID
    // 1. 创建套接字
    c_fd = socket(AF_INET, SOCK_STREAM, 0);  // 创建IPv4 TCP套接字
    if(c_fd == -1){              // 检查套接字创建是否成功
        perror("socket");        // 输出错误信息
        exit(-1);                // 退出程序
    }

    // 设置服务器地址信息
    c_addr.sin_family = AF_INET;                       // 使用IPv4地址族
    c_addr.sin_port = htons(atoi(argv[2]));            // 设置端口号(转换为网络字节序)
    inet_aton(argv[1],&c_addr.sin_addr);               // 将字符串IP地址转换为二进制格式

    // 2.连接到服务器
    if(connect(c_fd, (struct sockaddr *)&c_addr,sizeof(struct sockaddr)) == -1){
        perror("connect");      // 输出连接错误信息
        exit(-1);               // 退出程序
    }
    while(1){  // 主循环

        // 创建子进程处理用户输入
        if(fork()==0){  // 子进程代码块
            while(1){   // 子进程循环
                memset(msg,0,sizeof(msg));     // 清空消息缓冲区
                printf("input: ");             // 提示用户输入
                gets(msg);                     // 获取用户输入
                write(c_fd,msg,strlen(msg));   // 将消息发送到服务器
            }
        }

        // 父进程代码块:读取服务器响应
        while(1){
            memset(readBuf,0,sizeof(readBuf));   // 清空读取缓冲区
            n_read = read(c_fd, readBuf, 128);   // 从服务器读取数据
            if(n_read == -1){                    // 检查读取是否出错
                perror("read");                  // 输出读取错误信息
            }else{
                printf("\nget:%s\n",readBuf);    // 输出从服务器接收到的数据
            }
        }
    }
    // 3.send(注释表明此处原本计划放置发送代码)

    // 4.read(注释表明此处原本计划放置读取代码)


    return 0;  // 程序结束(实际上由于上面的无限循环,不会执行到这里)
}

重点

  • socket函数 创建 socket 网络数据交换的门户
  • bind函数 和socket关联起来
    • 填充地址结构体 sockaddr_in
    • inet_addr("192.168.1.100"):将点分十进制的IP字符串转换为网络字节序的32位整数。
    • my_addr.sin_addr.s_addr = htonl(INADDR_ANY); // 绑定到所有本地IP
  • listen函数 监听 将一个已绑定的 socket 从“主动”转换为“被动”模式,并通知操作系统开始监听来自客户端的连接请求
  • accept函数 从已完成连接队列(Accept Queue)中取出第一个连接请求,为这个连接创建一个新的socket,并返回其文件描述符。
  • inet_aton函数 将点分十进制格式的IPv4地址字符串
  • inet_ntoa函数 将网络格式的ip地址转为字符串格式
  • connect函数  主动与服务器建立连接
  • gets 函数 标准输入
  • sprintf函数 格式化数据写入字符串
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇