辽阳市网站建设_网站建设公司_GitHub_seo优化
2025/12/25 5:26:54 网站建设 项目流程

目录

一、TCP 的 “三大通信模型”

1.CS 模型(Client-Server)

2.BS 模型(Browser-Server)

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

二、TCP 的核心特征

三、TCP 的核心交互

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

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

四、TCP 的 “黏包” 问题

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

1.TCP 函数调用顺序

2.TCP 相关函数

2.1 创建套接字:socket()

2.2 绑定 IP 和端口:bind()

2.3 监听端口:listen()

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

2.5 接收数据:recv()

2.6 发送数据:send()

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

六、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()

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()

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()

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

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

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()

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()

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()

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)

#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)

#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.编译运行

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

4.运行结果

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询