TCP 传输控制协议笔记整理
一、网络模型
1. CS模型 (Client-Server)
-
客户端(Client):专用客户端程序
-
服务器(Server):提供服务
-
特点:
-
专用客户端
-
使用自定义协议
-
功能相对复杂
-
资源在本地
-
2. BS模型 (Browser-Server)
-
浏览器(Browser):通用客户端
-
服务器(Server):提供服务
-
特点:
-
通用客户端(浏览器)
-
使用HTTP协议
-
功能相对简单
-
资源由服务器发送
-
3. P2P模型 (Peer-to-Peer)
-
特点:
-
既是客户端也是服务器
-
同时下载和上传
-
网络下载工具常用
-
新客户端可以从其他客户端下载数据
-
二、TCP协议特点
1. 基本特征
-
有连接:需要三次握手建立连接
-
可靠传输:通过确认机制保证数据完整
-
流式套接字:数据没有边界,连续有序
-
全双工:双缓冲区,可同时收发
-
感知断开:能感知对方断开(四次挥手)
2. 可靠性保证
1. 应答机制 (ACK) - 确认收到数据
2. 超时重传 - 未收到确认则重发
3. 实时性弱 - 为保证可靠牺牲实时性
4. 网络资源消耗大 - 需要更多资源维护连接
3. 黏包问题
-
现象:TCP数据无边界,接收方可能一次性接收多个数据包
-
原因:发送方多次发送,接收方一次性接收
-
解决方案:需要应用层协议定义数据边界
三、TCP函数详解
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表示自适应
2. bind() - 绑定地址
int bind(int sockfd, struct sockaddr *my_addr, socklen_t addrlen);
功能:将套接字与地址绑定(服务器端)
3. listen() - 监听连接
int listen(int sockfd, int backlog);
参数:
backlog:允许三次握手的排队数
4. accept() - 接受连接
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
参数说明:
-
addr:客户端地址-
NULL:不关心客户端信息 -
非
NULL:存储客户端信息
-
-
addrlen:地址长度-
addr为NULL时,该值也为NULL -
非
NULL时:len = sizeof(struct sockaddr)
-
5. recv() - 接收数据
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
参数说明:
-
sockfd:-
服务器:
accept()返回值 -
客户端:
socket()返回值
-
-
flags:接收方式,0表示阻塞接收 -
返回值:实际接收的数据长度
6. send() - 发送数据
int send(int sockfd, const void *msg, size_t len, int flags);
参数说明:
-
sockfd:-
服务器:
accept()返回值 -
客户端:
socket()返回值
-
7. connect() - 连接服务器
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
功能:客户端连接服务器
四、TCP编程流程
服务器端流程:
socket() -> bind() -> listen() -> accept() -> recv()/send() -> close()
客户端流程:
socket() -> connect() -> send()/recv() -> close()
五、重要概念
1. 监听套接字 vs 通信套接字
-
监听套接字:用于接受连接请求
-
通信套接字:用于实际数据传输
-
关系:一个监听套接字可生成多个通信套接字
2. 连接建立与断开
三次握手建立连接:
客户端 -> SYN -> 服务器
客户端 <- SYN+ACK <- 服务器
客户端 -> ACK -> 服务器
四次挥手断开连接:
主动方 -> FIN -> 被动方
被动方 <- ACK <- 被动方
被动方 -> FIN -> 主动方
主动方 <- ACK <- 主动方
六、函数调用顺序表
| 步骤 | 服务器端 | 客户端 |
|---|---|---|
| 1 | socket() | socket() |
| 2 | bind() | |
| 3 | listen() | |
| 4 | accept() | connect() |
| 5 | recv()/send() | send()/recv() |
| 6 | close() | close() |
七、注意事项
-
写阻塞:缓冲区满时会发生写阻塞(约64K)
-
收发次数:发送次数和接收次数不需要对应
-
地址处理 :使用
inet_ntoa()和htons()进行地址转换 -
错误处理:所有网络函数都应检查返回值
-
资源释放:使用完毕后关闭套接字和文件描述符
八、示例代码框架
服务器框架:
int main() {
// 1. 创建socket
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
// 2. 绑定地址
struct sockaddr_in addr;
bind(sockfd, (struct sockaddr*)&addr, sizeof(addr));
// 3. 监听
listen(sockfd, 5);
// 4. 接受连接
int connfd = accept(sockfd, NULL, NULL);
// 5. 通信
char buf[1024];
recv(connfd, buf, sizeof(buf), 0);
send(connfd, buf, strlen(buf), 0);
// 6. 关闭
close(connfd);
close(sockfd);
}
客户端框架:
int main() {
// 1. 创建socket
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
// 2. 连接服务器
struct sockaddr_in server_addr;
connect(sockfd, (struct sockaddr*)&server_addr, sizeof(server_addr));
// 3. 通信
send(sockfd, "hello", 5, 0);
recv(sockfd, buf, sizeof(buf), 0);
// 4. 关闭
close(sockfd);
}