Linux系统编程——网络:TCP 协议与通信实战

目录

[一、TCP 的 "三大通信模型"](#一、TCP 的 “三大通信模型”)

[1.CS 模型(Client-Server)](#1.CS 模型(Client-Server))

[2.BS 模型(Browser-Server)](#2.BS 模型(Browser-Server))

[3.P2P 模型(Peer-to-Peer)](#3.P2P 模型(Peer-to-Peer))

[二、TCP 的核心特征](#二、TCP 的核心特征)

[三、TCP 的核心交互](#三、TCP 的核心交互)

1.三次握手(建立连接)

2.四次挥手(断开连接)

[四、TCP 的 "黏包" 问题](#四、TCP 的 “黏包” 问题)

[五、Linux 下 TCP 通信的 "流程 + 函数"](#五、Linux 下 TCP 通信的 “流程 + 函数”)

[1.TCP 函数调用顺序](#1.TCP 函数调用顺序)

[2.TCP 相关函数](#2.TCP 相关函数)

[2.1 创建套接字:socket()](#2.1 创建套接字:socket())

[2.2 绑定 IP 和端口:bind()](#2.2 绑定 IP 和端口:bind())

[2.3 监听端口:listen()](#2.3 监听端口:listen())

[2.4 接受客户端连接:accept()](#2.4 接受客户端连接:accept())

[2.5 接收数据:recv()](#2.5 接收数据:recv())

[2.6 发送数据:send()](#2.6 发送数据:send())

[2.7 客户端自动连接服务器:connect()](#2.7 客户端自动连接服务器:connect())

[六、TCP 通信代码示例](#六、TCP 通信代码示例)

1.服务端代码(server.c)

2.客户端代码(client.c)

3.编译运行

4.运行结果


一、TCP 的 "三大通信模型"

1.CS 模型(Client-Server)

  • 客户端是专用程序(比如手机里的微信 App),服务器是后台服务;
  • 协议可以自定义(比如微信自己的通信协议);
  • 资源大多存在客户端本地,功能相对复杂。

2.BS 模型(Browser-Server)

  • 客户端是通用浏览器(Chrome、Edge),服务器是 Web 服务;
  • 协议固定用 HTTP/HTTPS;
  • 资源由服务器发给浏览器,功能相对简单(毕竟依赖浏览器能力)。

3.P2P 模型(Peer-to-Peer)

  • 没有明确的 "客户端 / 服务器" 之分,每个节点既是下载者(客户端)也是上传者(服务器);
  • 典型场景:网络下载工具。

二、TCP 的核心特征

  1. 有连接:通信前必须通过 "三次握手" 建立连接,断开要 "四次挥手";
  2. 可靠传输:丢包会超时重传,接收方会发 ACK 确认,保证数据不丢不缺;
  3. 流式数据:数据是 "连续无边界" 的字节流(这也是 "黏包" 问题的根源);
  4. 全双工:双方可以同时收发数据;
  5. 拥塞控制:会根据网络状况调整发送速度,避免堵死。

三、TCP 的核心交互

TCP 的交互流程如下:

重点在于三次握手和四次挥手过程。

1.三次握手(建立连接)

TCP 是 "面向连接" 的协议,通信前必须通过三次握手初始化连接、同步序列号,确保双方收发能力正常。

三次握手流程:
三次握手流程

  • **第一次握手:**客户端给服务器发 SYN 报文,请求建立连接,同时告诉服务器 "我的初始序列号是 x";此时客户端进入 SYN_SENT 状态。
  • **第二次握手:**服务器收到 SYN 后,回复 SYN+ACK 报文 ------ 既确认收到客户端的请求(ACK=x+1),也告诉客户端 "我的初始序列号是 y";此时服务器进入 SYN_RCVD 状态。
  • **第三次握手:**客户端收到 SYN+ACK 后,发 ACK 报文确认(ACK=y+1);此时客户端进入ESTABLISHED(已连接)状态,服务器收到后也进入该状态,连接正式建立。

为什么需要三次?

核心是确认双方的收发能力都正常:客户端知道自己能发、服务器能收;服务器知道自己能收能发;客户端最终确认服务器能发、自己能收。少一次都无法完成双向确认。

2.四次挥手(断开连接)

TCP 是全双工通信,断开连接时双方都要独立关闭自己的发送通道,因此需要四次交互。

四次挥手流程:
四次挥手流程

  • **第一次挥手:**客户端发 FIN 报文,请求关闭自己的发送通道;客户端进入FIN_WAIT_1状态。
  • **第二次挥手:**服务器收到 FIN 后,发 ACK 报文确认;服务器进入 CLOSE_WAIT 状态,客户端进入 FIN_WAIT_2 状态(此时客户端只能收、不能发,服务器还能发数据)。
  • **第三次挥手:**服务器发完剩余数据后,发 FIN 报文请求关闭自己的发送通道;服务器进入 LAST_ACK 状态。
  • **第四次挥手:**客户端收到 FIN 后,发 ACK 报文确认,同时等待 2MSL(最大报文生存时间);客户端进入 TIME_WAIT,服务器收到 ACK 后进入 CLOSED,客户端等待超时后也进入 CLOSED。

为什么需要四次?

因为 TCP 是全双工 ------ 客户端说 "我不发了",服务器可能还没发完数据,得等服务器发完,再告诉客户端 "我也不发了",才能彻底断开。

四、TCP 的 "黏包" 问题

因为 TCP 是 "流式数据",发送方分 10 次发的内容,接收方可能一次全收了,导致数据混在一起 ------ 这就是黏包

解决办法有 3 种:

  1. 自定义结束标志:比如每个数据包末尾加 "\r\n",接收方读到标志就分割;
  2. 固定数据包大小:用 struct 定义固定长度的结构体,接收方按固定长度读;
  3. 自定义协议头:比如设计一个协议格式:AA(起始符) + Num(数据长度) + Data(数据) + 校验 + BB(结束符),接收方先读头、再读对应长度的数据。

五、Linux 下 TCP 通信的 "流程 + 函数"

1.TCP 函数调用顺序

2.TCP 相关函数

2.1 创建套接字:socket()

cpp 复制代码
int socket(int domain, int type, int protocol);
  • **功能:**程序向内核提出创建一个基于内存的套接字描述符
  • 参数:
    • domain:地址族
      • PF_INET == AF_INET:适用于互联网程序
      • PF_UNIX == AF_UNIX:适用于单机进程间通信
    • type:套接字类型
      • SOCK_STREAM:流式套接字,对应 TCP 协议
      • SOCK_DGRAM:用户数据报套接字,对应 UDP 协议
      • SOCK_RAW:原始套接字,对应 IP 协议
    • protocol:协议类型,填 0 表示自动适应用层协议
  • 返回值:
    • 成功:返回申请到的套接字 ID
    • 失败:返回 -1

2.2 绑定 IP 和端口:bind()

cpp 复制代码
int bind(int sockfd, struct sockaddr *my_addr, socklen_t addrlen);
  • **功能:**仅服务器端调用,将参数 1 对应的套接字文件描述符,与参数 2 指定的接口地址进行绑定,用于从该接口接收数据。
  • 参数:
    • sockfd:目标套接字的 ID
    • my_addr:需要绑定的接口地址信息
    • addrlen:my_addr 对应的结构体长度
  • 返回值:
    • 成功:返回 0
    • 失败:返回 -1

2.3 监听端口:listen()

cpp 复制代码
int listen(int sockfd, int backlog);
  • **功能:**在参数 1 对应的套接字 ID 上,开启监听状态,等待客户端的连接请求。
  • 参数:
    • sockfd:目标套接字的 ID
    • backlog:允许处于三次握手过程中的连接排队数量
  • 返回值:
    • 成功:返回 0
    • 失败:返回 -1

2.4 接受客户端连接:accept()

cpp 复制代码
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
  • **功能:**从已监听的连接队列中,取出一个有效的客户端连接,并将其接入当前程序。
  • 参数:
    • sockfd:处于监听状态的套接字 ID
    • addr:用于存储客户端地址信息的结构体指针
      • 若填 NULL,表示不记录客户端信息
      • 若需记录,需先定义变量并传入其地址,函数会自动将客户端信息存入该变量
    • addrlen:addr 对应的结构体长度
      • 若addr为 NULL,该值也填 NULL
      • 若addr不为 NULL,需先将其赋值为 sizeof(struct sockaddr)
  • 返回值:
    • 成功:返回一个新的套接字 ID(后续与该客户端的通信,均基于此 ID)
    • 失败:返回 -1

2.5 接收数据:recv()

cpp 复制代码
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
  • **功能:**从指定的套接字中,以 flags 指定的方式,读取长度为 len 的字节数据,并存入 buf 对应的内存中。
  • 参数:
    • sockfd:目标套接字的 ID
      • 服务器端:填 accept 返回的新套接字 ID
      • 客户端:填 socket 返回的套接字 ID
    • buf:用于存储数据的本地内存(通常为数组或动态分配内存)
    • len:期望读取的数据长度
    • flags:数据读取方式,填0表示阻塞式接收
  • 返回值:
    • 成功:返回实际接收到的数据长度(通常≤len)
    • 失败:返回 -1

2.6 发送数据:send()

cpp 复制代码
int send(int sockfd, const void *msg, size_t len, int flags);
  • **功能:**从 msg 对应的内存中,取出长度为 len 的数据,以 flags 指定的方式,写入到 spckfd 对应的套接字中。
  • 参数:
    • sockfd:目标套接字的 ID
      • 服务器端:填 accept 返回的新套接字 ID
      • 客户端:填 socket 返回的套接字 ID
    • msg:需要发送的消息内容
    • len:需要发送的消息长度
    • flags:消息发送方式
  • 返回值:
    • 成功:返回实际发送的字符长度
    • 失败:返回 -1

2.7 客户端自动连接服务器:connect()

cpp 复制代码
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
  • **功能:**仅客户端调用,向 addr 对应的远程目标主机,发起连接请求(触发 TCP 三次握手)。
  • 参数:
    • sockfd:本地 socket 创建的套接字 ID
    • addr:远程目标主机的地址信息
    • addrlen:addr 对应的结构体长度
  • 返回值:
    • 成功:返回 0
    • 失败:返回 -1

六、TCP 通信代码示例

1.服务端代码(server.c)

cpp 复制代码
#include <netinet/in.h>
#include <netinet/ip.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <time.h>
#include <unistd.h>

typedef struct sockaddr *(SA);

int main(int argc, char **argv)
{
  // 监听套接字,用来三次握手
  int listfd = socket(AF_INET, SOCK_STREAM, 0);
  if (-1 == listfd)
  {
    perror("socket");
    return 1;
  }

  struct sockaddr_in ser, cli;
  bzero(&ser, sizeof(ser));
  bzero(&cli, sizeof(cli));
  ser.sin_family = AF_INET;
  ser.sin_port = htons(50000);
  ser.sin_addr.s_addr = INADDR_ANY;

  int ret = bind(listfd, (SA)&ser, sizeof(ser));
  if (-1 == ret)
  {
    perror("bind");
    return 1;
  }

  // 3代表建立连接(三次握手)的排队数
  listen(listfd, 3);
  socklen_t len = sizeof(cli);

  // 通信套接字,表示客户端
  int conn = accept(listfd, (SA)&cli, &len);
  if (-1 == conn)
  {
    perror("accept");
    return 1;
  }

  while (1)
  {
    char buf[512] = {0};
    int ret = recv(conn, buf, sizeof(buf), 0);
    if (ret <= 0)
    {
      break;
    }
    time_t tm;
    time(&tm);
    struct tm *info = localtime(&tm);
    sprintf(buf, "%s %d:%d:%d", buf, info->tm_hour, info->tm_min, info->tm_sec);
    send(conn, buf, strlen(buf), 0);
  }
  close(listfd);
  close(conn);

  return 0;
}

2.客户端代码(client.c)

cpp 复制代码
#include <netinet/in.h>
#include <netinet/ip.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <time.h>
#include <unistd.h>

typedef struct sockaddr *(SA);

int main(int argc, char **argv)
{
  int sockfd = socket(AF_INET, SOCK_STREAM, 0);
  if (-1 == sockfd)
  {
    perror("socket");
    return 1;
  }
  struct sockaddr_in ser;
  bzero(&ser, sizeof(ser));
  ser.sin_family = AF_INET;
  ser.sin_port = htons(50000);
  ser.sin_addr.s_addr = INADDR_ANY;

  int ret = connect(sockfd, (SA)&ser, sizeof(ser));
  if (-1 == ret)
  {
    perror("connect");
    return 1;
  }

  int i = 10;
  while (i--)
  {
    char buf[512] = "this is tcp test";
    send(sockfd, buf, strlen(buf), 0);
    bzero(buf, sizeof(buf));
    recv(sockfd, buf, sizeof(buf), 0);
    printf("ser:%s\n", buf);
    sleep(1);
  }
  close(sockfd);

  return 0;
}

3.编译运行

bash 复制代码
# 编译服务端
gcc tcp_server.c -o server
# 编译客户端
gcc tcp_client.c -o client
 
# 启动服务端
./server
# 另开终端启动客户端
./client

4.运行结果

相关推荐
qq_411262421 小时前
用 ESP32-C3 直接连 Starlink 路由器/热点并完成配网
网络·智能路由器
郝亚军1 小时前
ubuntu-18.04.6-desktop-amd64安装步骤
linux·运维·ubuntu
梁辰兴1 小时前
计算机网络基础:TCP 的拥塞控制
tcp/ip·计算机网络·php·tcp·拥塞控制·计算机网络基础·梁辰兴
Konwledging2 小时前
kernel-devel_kernel-headers_libmodules
linux
Web极客码2 小时前
CentOS 7.x如何快速升级到CentOS 7.9
linux·运维·centos
一位赵2 小时前
小练2 选择题
linux·运维·windows
代码游侠2 小时前
学习笔记——Linux字符设备驱动开发
linux·arm开发·驱动开发·单片机·嵌入式硬件·学习·算法
LucDelton3 小时前
Java 读取无限量文件读取的思路
java·运维·网络
Lw老王要学习3 小时前
CentOS 7.9达梦数据库安装全流程解析
linux·运维·数据库·centos·达梦
CRUD酱3 小时前
CentOS的yum仓库失效问题解决(换镜像源)
linux·运维·服务器·centos