一、网络通信模式
1. C/S 模式(客户端 / 服务器模式)
- 核心思想:客户端主动发起请求,服务器被动接收并响应请求,是 TCP 通信的典型模式。
- 特点:服务器需要长期运行,客户端按需启动;资源集中在服务器,客户端仅负责交互。
- **交互逻辑:**客户端与服务器点对点专属通信。例如:QQ连接QQ服务器、打游戏
2. B/S 模式(浏览器 / 服务器模式)
- 核心思想 :基于 HTTP 协议(底层仍为 TCP),以浏览器作为通用客户端,无需开发专用客户端。
- 特点:跨平台性强,开发成本低;依赖网络和浏览器环境。
- **交互逻辑:**浏览器向服务器发起请求,服务器响应后返回页面。例如淘宝、京东网页
3. P2P 模式(对等网络模式)
- 核心思想:无中心服务器,每个节点既是客户端也是服务器,节点间直接通信。
- 特点:去中心化,资源利用率高;节点管理和连接稳定性较难控制。
二、TCP 核心特征
- 面向连接 :通信前必须通过 "三次握手" 建立连接,通信结束需 "四次挥手" 释放连接。
- 可靠传输 :通过序列号、确认应答、重传机制、流量控制、拥塞控制保证数据不丢失、不重复、按序到达。
- 面向字节流 :以字节为单位传输数据,无数据边界(易引发黏包问题)。
- 全双工通信:同一连接中,双方可同时收发数据。
- 拥塞控制:通过慢启动、拥塞避免等算法,适配网络拥塞状态。
三、TCP 会话过程
1. 三次握手(建立连接)
SYN:同步标志、ACK:确认标志、FIN:结束标志、seq:序列号、ack:确认号
| 握手阶段 | 发起方 | 核心参数(TCP 报文段) | 参数意义 |
|---|---|---|---|
| 第一次 | 客户端 | SYN=1,seq=x | 客户端向服务器发起连接请求,SYN 置 1 表示请求同步,seq 为客户端初始序列号 |
| 第二次 | 服务器 | SYN=1,ACK=1,seq=y,ack=x+1 | 服务器确认客户端请求(ACK=1 表示确认,ack=x+1 表示期望接收下一字节),同时向客户端发起连接请求(SYN=1,seq=y) |
| 第三次 | 客户端 | ACK=1,seq=x+1,ack=y+1 | 客户端确认服务器的连接请求,连接正式建立 |
2. 四次挥手(释放连接)
| 挥手阶段 | 发起方 | 核心参数 | 参数意义 |
|---|---|---|---|
| 第一次 | 客户端 | FIN=1,seq=u | 客户端向服务器发送关闭连接请求(FIN=1 表示无数据要发送),seq 为当前序列号 |
| 第二次 | 服务器 | ACK=1,seq=v,ack=u+1 | 服务器确认客户端的关闭请求,此时服务器仍可向客户端发数据 |
| 第三次 | 服务器 | FIN=1,ACK=1,seq=w,ack=u+1 | 服务器无数据发送,向客户端发送关闭请求 |
| 第四次 | 客户端 | ACK=1,seq=u+1,ack=w+1 | 客户端确认服务器的关闭请求,等待 2MSL 后释放连接 |

四、C/S 模式编程流程(以 Linux 为例)
通用前置:头文件
#include <sys/socket.h> // 套接字核心函数
#include <netinet/in.h> // 地址结构体定义
#include <arpa/inet.h> // IP 地址转换函数
#include <unistd.h> // close 函数
#include <string.h> // 内存操作(如 memset)

1. 服务器端流程
(1)创建 TCP 监听套接字:socket ()
- 函数原型 :
int socket(int domain, int type, int protocol); - 功能:创建一个套接字文件描述符,用于后续网络通信。
- 参数 :
domain:地址族,TCP 用AF_INET(IPv4)/AF_INET6(IPv6);type:套接字类型,TCP 用SOCK_STREAM(流式套接字),UDP 用SOCK_DGRAM;protocol:协议类型,填 0 表示自动匹配 type 对应的默认协议(TCP 为 IPPROTO_TCP)。
- 返回值:成功返回非负套接字描述符;失败返回 -1,设置 errno。
(2)绑定 IP + 端口:bind ()
- 函数原型 :
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen); - 功能:将套接字与指定的 IP 地址和端口号绑定。
- 参数 :
sockfd:socket () 返回的套接字描述符;addr:通用地址结构体指针,需强转为struct sockaddr_in(IPv4)类型,包含 IP 和端口;addrlen:地址结构体的长度,sizeof(struct sockaddr_in)。
- 返回值:成功返回 0;失败返回 -1,设置 errno。
(3)进入监听状态:listen ()
- 函数原型 :
int listen(int sockfd, int backlog); - 功能:将套接字转为监听状态,等待客户端连接请求。
- 参数 :
sockfd:绑定后的套接字描述符;backlog:半连接队列(未完成三次握手)的最大长度(如 5、10,具体值受系统限制)。
- 返回值:成功返回 0;失败返回 -1,设置 errno。
(4)建立连接:accept ()
- 函数原型 :
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen); - 功能:阻塞等待客户端连接,建立后返回新的套接字用于与该客户端通信。
- 参数 :
sockfd:监听套接字描述符;addr:输出参数,存储客户端的 IP 和端口信息;addrlen:输入输出参数,传入结构体长度,传出实际长度。
- 返回值:成功返回新的通信套接字描述符;失败返回 -1,设置 errno。
(5)接收数据:recv ()
- 函数原型 :
ssize_t recv(int sockfd, void *buf, size_t len, int flags); - 功能:从已连接的套接字接收数据。
- 参数 :
sockfd:accept () 返回的通信套接字;buf:存储接收数据的缓冲区;len:缓冲区最大长度;flags:接收方式,0 表示阻塞接收,MSG_DONTWAIT 表示非阻塞。
- 返回值:成功返回实际接收的字节数;0 表示客户端关闭连接;-1 表示失败,设置 errno。
(6)发送数据:send ()
- 函数原型 :
ssize_t send(int sockfd, const void *buf, size_t len, int flags); - 功能:向已连接的套接字发送数据。
- 参数 :
sockfd:通信套接字描述符;buf:待发送数据的缓冲区;len:待发送数据的长度;flags:发送方式,0 表示阻塞发送。
- 返回值:成功返回实际发送的字节数;失败返回 -1,设置 errno。
(7)关闭连接:close ()
- 函数原型 :
int close(int fd); - 功能:关闭套接字描述符,释放资源。
- 参数 :
fd:待关闭的套接字描述符(监听 / 通信套接字)。 - 返回值:成功返回 0;失败返回 -1,设置 errno。
代码:
cs
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <time.h>
typedef struct sockaddr *(SA);
int main(int argc, char **argv)
{
//创建监听套接字
int listfd = socket(AF_INET,SOCK_STREAM,0);
if(listfd == -1)
{
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;
//给套接字绑定ip+port
int ret = bind(listfd,(SA)&ser,sizeof(ser));
if(ret == -1)
{
perror("bind");
return 1;
}
//进入监听状态
listen(listfd, 3);
socklen_t len = sizeof(cli);
//建立连接-接收通信套接字
int conn = accept(listfd,(SA)&cli,&len);
if(conn == -1)
{
perror("accept");
return 1;
}
while(1)
{
char buf[521] = {0};
//接收数据
int rd_ret = recv(conn,buf,sizeof(buf),0);
if(rd_ret <= 0)
{
break;
}
//处理数据
printf("cli:%s\n",buf);
time_t tm;
time(&tm);
sprintf(buf, "%s %s",buf,ctime(&tm));
//发送数据
send(conn,buf,strlen(buf),0);
}
//断开连接
close(listfd);
close(conn);
return 0;
}
2. 客户端流程
(1)创建套接字:socket ()
- 同服务器端,参数一致(AF_INET + SOCK_STREAM + 0)。
(2)构造服务器地址结构体
struct sockaddr_in server_addr;
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET; // 地址族
server_addr.sin_port = htons(8080); // 服务器端口(网络字节序)
server_addr.sin_addr.s_addr = inet_addr("127.0.0.1"); // 服务器 IP
(3)主动连接服务器:connect ()
- 函数原型 :
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen); - 功能:向服务器发起 TCP 连接请求(三次握手)。
- 参数 :
sockfd:客户端套接字描述符;addr:服务器地址结构体指针;addrlen:地址结构体长度。
- 返回值:成功返回 0;失败返回 -1,设置 errno。
(4)发送 / 接收数据:send ()/recv ()
- 同服务器端,参数一致(使用客户端套接字)。
(5)关闭连接:close ()
- 同服务器端。
代码:
cs
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <time.h>
#include <arpa/inet.h>
typedef struct sockaddr *(SA);
int main(int argc, char **argv)
{
//创建套接字
int sockfd = socket(AF_INET,SOCK_STREAM,0);
if(sockfd == -1)
{
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 = inet_addr("192.168.31.136");
//主动连接服务器
int ret = connect(sockfd, (SA)&ser, sizeof(ser));
if(ret == -1)
{
perror("connect");
return 1;
}
int i = 30;
while(i--)
{
char buf[512] = "hello,this is why";
//发送数据
send(sockfd,buf,strlen(buf),0);
bzero(buf, sizeof(buf));
//接收数据
int rd_ret = recv(sockfd, buf, sizeof(buf), 0);
if(rd_ret <= 0)
{
break;
}
printf("from ser:%s\n",buf);
sleep(1);
}
//断开连接
close(sockfd);
return 0;
}
五、TCP 黏包问题
1. 原因
- TCP 是面向字节流,无数据边界,操作系统会根据缓冲区情况合并 / 拆分数据;
- 发送方:操作系统会为了效率,把多次小数据合并成一个数据包发送(Nagle算法);
- 接收方:缓冲区未及时读取,多个数据包被一次性读取。
- 黏包 = TCP 字节流特性 + 发送方缓冲区攒包 + 接收方缓冲区累积
2. 现象
- 接收方一次读取到多个发送方的数据包(如发送 "hello" + "world",接收 "helloworld");
- 接收方一次读取到不完整的数据包(如发送 100 字节,只读取到 50 字节)。
3. 解决方案
- 固定长度包:双方约定数据包长度(如每次发 1024 字节),不足补空,接收方按固定长度读取;
- 添加分隔符:数据包末尾加特殊分隔符(如 \r\n),接收方按分隔符拆分;
- 自定义协议头 :包头包含数据长度(如 4 字节表示包体长度),接收方先读包头,再按长度读包体(最常用)。

六、TCP 与 UDP 对比
| 特性 | TCP | UDP |
|---|---|---|
| 连接性 | 面向连接(三次握手) | 无连接 |
| 可靠性 | 可靠(确认、重传、有序) | 不可靠(无确认,易丢包) |
| 数据边界 | 无(字节流),易黏包 | 有(数据报),无黏包 |
| 通信方式 | 全双工 | 单工 / 半双工(按需) |
| 速度 | 慢(有拥塞 / 流量控制) | 快(无额外控制) |
| 适用场景 | 文件传输、网页加载、聊天 | 音视频直播、游戏、广播 |
| 核心函数 | socket/bind/listen/accept | socket/bind/sendto/recvfrom |
总结
- TCP 核心是面向连接、可靠传输,C/S 编程需遵循 "创建套接字 - 绑定 - 监听 - 连接 - 收发 - 关闭" 流程,核心函数(socket/bind/listen/accept/connect/recv/send)需掌握原型、参数和返回值;
- 黏包问题源于 TCP 字节流特性,解决方案核心是给数据加边界(固定长度、分隔符、自定义协议头);
- TCP 与 UDP 核心差异在 "连接性" 和 "可靠性",需根据场景选择(可靠选 TCP,高速选 UDP)。