网络编程:TCP Socket

1 核心概念

1.1 套接字 (Socket)
  • 定义:Socket 是网络通信的端点 (Endpoint)。在 Linux 内核中,Socket 被抽象为一种文件。
  • 本质:应用程序通过 Socket 文件描述符与内核的网络协议栈进行交互。
  • 分类
    • 流式套接字 (SOCK_STREAM) :基于 TCP 协议。提供面向连接、可靠、无差错、无重复、按序到达的字节流传输。
    • 数据报套接字 (SOCK_DGRAM) :基于 UDP 协议。提供无连接、不可靠、固定最大长度的数据报传输。
1.2 网络字节序 (Network Byte Order)
  • 问题背景 :不同 CPU 架构对多字节整数的存储方式不同。
    • 小端序 (Little-Endian):低位字节存放在低地址(x86/ARM 常为此类)。
    • 大端序 (Big-Endian):高位字节存放在低地址。
  • 标准规定 :TCP/IP 网络传输协议统一规定使用 大端序 (Big-Endian)
  • 转换函数
    • htons() (Host to Network Short): 16位主机序转网络序(常用于端口)。
    • htonl() (Host to Network Long): 32位主机序转网络序(常用于IP)。
    • ntohs() / ntohl(): 网络序转主机序。
1.3 IP 地址与端口
  • IP 地址:用于在网络层唯一标识一台主机(IPv4 为 32 位整数)。
  • 端口 (Port):用于在传输层区分同一主机上的不同应用程序进程(16 位整数,范围 0-65535)。

2 关键数据结构

需包含头文件:#include <netinet/in.h>

实际上,这些结构体做了比较复杂的封装,下面出现的形式是简化后的。

2.1 通用地址结构体

这是 Linux 网络 API (如 bind, connect) 使用的通用参数类型。所有具体的协议地址结构体都必须强制类型转换为此类型。

c 复制代码
struct sockaddr {
    unsigned short sa_family;    // 地址族 (如 AF_INET)
    char           sa_data[14];  // 协议地址数据 (混合了IP和端口)
};
2.2 IPv4 专用地址结构体

这是在代码中实际填充的结构体,填充完毕后强转为 struct sockaddr * 传给 API。

c 复制代码
struct sockaddr_in {
    short int          sin_family;  // 地址族 (必须设为 AF_INET)
    unsigned short int sin_port;    // 端口号 (必须是网络字节序, 使用 htons)
    struct in_addr     sin_addr;    // IP 地址结构体
    unsigned char      sin_zero[8]; // 填充字节 (必须置0,为了与 struct sockaddr 对齐)
};

// 其中 IP 地址结构体定义如下:
struct in_addr {
    unsigned long s_addr; // 32位 IP 地址 (网络字节序)
};

常用赋值模式:

c 复制代码
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(8888);
addr.sin_addr.s_addr = INADDR_ANY; // 自动绑定本机所有网卡 IP
// 或者指定 IP: inet_pton(AF_INET, "192.168.1.10", &addr.sin_addr);

3 核心 API

需包含头文件:#include <sys/socket.h>

3.1 创建套接字
c 复制代码
int socket(int domain, int type, int protocol);
  • 功能:创建一个通信端点并返回一个描述符。
  • 参数
    • domain:协议族。AF_INET (IPv4), AF_INET6 (IPv6), AF_UNIX (本地 IPC)。
    • type:套接字类型。SOCK_STREAM (TCP), SOCK_DGRAM (UDP)。
    • protocol:具体协议,通常传 0(系统根据 type 自动选择)。
  • 返回值:成功返回非负文件描述符 (fd),失败返回 -1 并设置 errno。
3.2 绑定地址(服务器端)
c 复制代码
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
  • 功能:将套接字与特定的本地地址 (IP + Port) 关联。
  • 注意addr 参数需要将 struct sockaddr_in* 强转为 struct sockaddr*
  • 返回值:成功返回 0,失败返回 -1。
3.3 监听连接(服务器端)
c 复制代码
int listen(int sockfd, int backlog);
  • 功能 :将套接字标记为被动 (Passive) 状态,用于接受传入的连接请求。
  • 参数
    • backlog:挂起连接队列的最大长度(即三次握手完成但尚未被 accept 取走的连接数)。
  • 返回值:成功返回 0,失败返回 -1。
3.4 接受连接(服务器端)
c 复制代码
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
  • 功能 :从监听队列中取出第一个连接请求。若队列为空,默认为阻塞 (Blocking) 等待。
  • 参数
    • addr:(传出参数) 用于存储客户端的地址信息。
    • addrlen:(传入传出参数) 传入 addr 的大小,函数返回实际写入的大小。
  • 返回值
    • 成功:返回一个新的文件描述符 (Connected Socket),专门用于与该客户端通信。
    • 失败:返回 -1。
  • 重要区分sockfd 是监听套接字(总机),返回值是已连接套接字(分机)。
3.5 发起连接(客户端)
c 复制代码
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
  • 功能:向指定地址的服务器发起 TCP 三次握手。
  • 返回值:成功返回 0,失败返回 -1(如超时、连接被拒)。
3.6 数据传输

在 Linux 中,Socket 就是文件,因此可以使用系统 I/O。

  • write(fd, buf, len):发送数据。
  • read(fd, buf, len) :接收数据。
    • 返回 > 0:实际读到的字节数。
    • 返回 0表示对端关闭了连接 (EOF)。这是判断断线的标准方式。
    • 返回 < 0:发生错误 (errno)。

4 地址转换函数

需包含头文件:#include <arpa/inet.h>

  1. 将字符串 IP (如 "192.168.1.1") 转换为二进制网络字节序结构(Presentation to Network)
c 复制代码
int inet_pton(int af, const char *src, void *dst);
  1. 将二进制网络字节序 IP 转换为字符串(Network to ASCII)
c 复制代码
char *inet_ntoa(struct in_addr in);

5 标准 TCP 编程模型

阶段 服务器端 (Server) 动作 客户端 (Client) 动作
1. 准备 socket() 创建套接字 socket() 创建套接字
2. 地址 填充 sockaddr_in (本机 IP+端口) 填充 sockaddr_in (服务器 IP+端口)
3. 绑定 bind() 绑定地址 (客户端通常不需要 bind,系统自动分配随机端口)
4. 等待 listen() 开启监听
5. 建立 accept() 阻塞等待连接 connect() 发起连接 (三次握手)
6. 通信 read() / write() write() / read()
7. 结束 close() (先关分机,再关总机) close()

6 TCP Echo 服务器实验

  1. Server (泰山派):开启 8888 端口监听,收到什么就回发什么(Echo)。
  2. Client (PC/虚拟机):连接泰山派,发送字符串。
1 编写服务器代码
c 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>

#define PORT 8888
#define BUFFER_SIZE 1024

int main()
{
    int server_fd, new_socket;
    struct sockaddr_in address; // IPv4 地址结构体
    int opt = 1;
    int addrlen = sizeof(address);
    char buffer[BUFFER_SIZE] = {0};

    printf("[Server] Creating socket...\n");
    // 1. 创建 Socket
    // AF_INET: IPv4
    // SOCK_STREAM: TCP (如果是 UDP 则用 SOCK_DGRAM)
    if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == 0)
    {
        perror("socket failed");
        exit(EXIT_FAILURE);
    }

    // 2. 绑定 (Bind) IP 和端口
    address.sin_family = AF_INET;
    address.sin_addr.s_addr = INADDR_ANY; // 监听本机所有网卡 (Wi-Fi, 网口)
    address.sin_port = htons(PORT);       // 注意:端口号必须转为网络字节序(Big Endian)

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

    // 3. 监听 (Listen)
    // 3: 待处理连接队列的最大长度
    if (listen(server_fd, 3) < 0)
    {
        perror("listen");
        exit(EXIT_FAILURE);
    }

    printf("[Server] Listening on port %d... Waiting for connection.\n", PORT);

    // 4. 接受连接 (Accept) - 会阻塞在这里,直到有客户端连上来
    // accept 返回一个新的 fd (new_socket) 专门用于和这个客户端通信
    if ((new_socket = accept(server_fd, (struct sockaddr *)&address, (socklen_t *)&addrlen)) < 0)
    {
        perror("accept");
        exit(EXIT_FAILURE);
    }

    // 打印客户端的 IP
    printf("[Server] Connection accepted from %s\n", inet_ntoa(address.sin_addr));

    // 5. 循环通信
    while (1)
    {
        memset(buffer, 0, BUFFER_SIZE);

        // 读取数据
        int valread = read(new_socket, buffer, BUFFER_SIZE);
        if (valread <= 0)
        {
            // read 返回 0 表示客户端断开了连接
            printf("[Server] Client disconnected.\n");
            break;
        }

        printf("[Server] Received: %s", buffer);

        // 回显数据 (Echo)
        char msg[BUFFER_SIZE + 32];
        sprintf(msg, "Echo: %s", buffer);
        send(new_socket, msg, strlen(msg), 0);
    }

    // 6. 关闭连接
    close(new_socket); // 关闭与客户端的连接
    close(server_fd);  // 关闭服务器监听
    return 0;
}
2 编写客户端代码
c 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>

#define PORT 8888
#define BUFFER_SIZE 1024

int main(int argc, char const *argv[])
{
    int sock = 0;
    struct sockaddr_in serv_addr;
    char buffer[BUFFER_SIZE] = {0};

    if (argc != 2)
    {
        printf("Usage: %s <Server_IP>\n", argv[0]);
        return -1;
    }

    // 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);

    // 2. 将 IP 字符串 (如 "192.168.1.10") 转为二进制网络格式
    if (inet_pton(AF_INET, argv[1], &serv_addr.sin_addr) <= 0)
    {
        printf("\nInvalid address / Address not supported \n");
        return -1;
    }

    printf("[Client] Connecting to %s:%d...\n", argv[1], PORT);

    // 3. 连接服务器 (Connect)
    if (connect(sock, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) < 0)
    {
        printf("\nConnection Failed \n");
        return -1;
    }

    printf("[Client] Connected! Type text to send.\n");

    // 4. 发送数据
    while (1)
    {
        printf("> ");
        memset(buffer, 0, BUFFER_SIZE);
        // 从键盘读取输入
        fgets(buffer, BUFFER_SIZE, stdin);

        // 发送
        send(sock, buffer, strlen(buffer), 0);

        // 接收回显
        memset(buffer, 0, BUFFER_SIZE);
        int valread = read(sock, buffer, BUFFER_SIZE);
        if (valread > 0)
        {
            printf("[Server Reply]: %s\n", buffer);
        }
        else
        {
            printf("Server disconnected.\n");
            break;
        }
    }

    close(sock);
    return 0;
}
3 编译与运行

交叉编译 Server (给泰山派),编译 Client (给 PC/Ubuntu),联网运行:

  1. 查询板子 IP: 在板子上输入 ifconfigip addr,找到 wlan0 (Wi-Fi) 或 eth0 (网线) 的 IP 地址。 假设是 192.168.31.200

  2. 启动 Server (板子端):

bash 复制代码
# ./tcp_server
[Server] Creating socket...
[Server] Listening on port 8888... Waiting for connection.
  1. 启动 Client (PC端):
bash 复制代码
$ ./main 192.168.31.200
[Client] Connecting to 192.168.173.65:8888...
[Client] Connected! Type text to send.
  1. 现象。

    • 在 PC 输入 Hello,板子终端会打印 [Server] Received: Hello

    • PC 终端会收到 [Server Reply]: Echo: Hello

    • 如果在 PC 按 Ctrl+C 退出,板子会检测到 read 返回 0,并打印 [Server] Client disconnected.

相关推荐
EXtreme352 小时前
【数据结构】手撕队列(Queue):从FIFO底层原理到高阶应用的全景解析
c语言·数据结构·链表·队列
程序喵大人2 小时前
Duff‘s device
c语言·开发语言·c++
互亿无线明明2 小时前
国际短信通知服务:如何为全球业务构建稳定的跨国消息触达体系?
java·c语言·python·php·objective-c·ruby·composer
深盾科技2 小时前
Linux跨进程内存操作的3种方法及防护方案
java·linux·网络
HalvmånEver2 小时前
Linux:基础IO(一)
linux·运维·服务器
Lynnxiaowen2 小时前
今天我们学习kubernetes内容持久化存储
linux·运维·学习·容器·kubernetes
Starry_hello world2 小时前
Linux 信号
linux
KingRumn2 小时前
Linux进程间通信之消息队列
linux·服务器·网络
jerryinwuhan2 小时前
1210_linux_2
linux·运维·服务器