一、TCP协议介绍
①TCP(Transmission Control Protocol):传输控制协议(流式套接字),位于传输层
②TCP特点
- 面向数据流
- 面向连接
- 安全可靠的传输协议
- TCP机制复杂
③三次握手
(1)目的:TCP 建立连接时需要进行三次握手,以确保通信安全可靠,并确认通信双方都已准备就绪。该过程必须由客户端发起。
(2)过程:三次握手由客户端发起。首先,客户端发送一个带有 SYN 标志(请求建立连接)的数据包;随后,服务端回复一个同时带有 ACK(响应报文)和 SYN 标志的数据包;最后,客户端再回复一个带有 ACK 标志的数据包,完成连接建立。
④四次挥手:
(1)目的:断开连接,确保通信双方在断开前已完成数据的收发。断开可由客户端或服务端发起。如果双方通信结束(不传输数据),此时四次挥手也可能变成三次挥手。
(2)过程(以客户端发起为例):
- 客户端发送一个带 FIN 标志的报文,请求断开连接(FIN 标志位于 TCP 头部)。
- 服务端回复带 ACK 标志的报文确认;若还有数据要发送,则以带 PSH 标志的报文继续发送,直至数据发送完毕。
- 数据发送完毕后,服务端向客户端发送带 FIN 标志的报文,请求断开连接。
- 客户端回复带 ACK 标志的报文,连接断开完成。
- 注意:TCP在四次挥手之前,默认保持TCP通信连接的状态
二、TCP函数接口
2.1 socket(创建套接字)
int socket(int domain, int type, int protocol); 功能:创建一个套接字 参数: domain:使用的协议族 AF_INET: IPv4 AF_INET6:IPv6 type:传输方式 SOCK_STREAM :流式套接字,TCP SOCK_DGRAM:数据报套接字,UDP SOCK_RAW: 原始套接字 protocol: 0 默认套接字类型对应的协议 返回值: 成功返回新的文件描述符 失败返回-1 tcp_socket = socket(AF_INET, SOCK_STREAM, 0); udp_socket = socket(AF_INET, SOCK_DGRAM, 0); raw_socket = socket(AF_INET, SOCK_RAW, protocol);
2.2 connect(建立连接)
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen); 功能:发送一个链接,请求建立连接 参数: sockfd:建立连接的套接字文件描述符 addr:目的空间地址,服务端的地址信息 addrlen:目的地址长度,服务端地址大小 返回值: 成功返回0 失败返回-1注意:有可能会发生阻塞
2.3 send(发送数据)
ssize_t send(int sockfd, const void *buf, size_t len, int flags); 功能:向套接字中发送数据 参数: sockfd:文件描述符 buf:发送数据空间的首地址 len:发送数据的长度 flags:属性, 默认为0 返回值: 成功返回发送字节个数 失败返回-1
2.4 recv(接收数据)
ssize_t recv(int sockfd, void *buf, size_t len, int flags); 功能:从套接字中接收数据 参数: sockfd:套接字文件描述符 buf:存放接收到的数据空间的首地址 len:最多接收元素个数 flags:属性默认为0 返回值: 成功返回接收字节的个数 失败返回-1 如果对方关闭连接, 则返回0
2.5 listen(监听)
int listen(int sockfd, int backlog); 功能:监听链接请求,监听三次握手 参数: sockfd:监听套接字文件描述符 backlog:尚未处理的请求的最大的个数,允许同时监听的客户端的最大个数,上限128 返回值: 成功返回0 失败返回-1
2.6 accept(接收客户端)
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen); 功能:处理监听队列中的第一个连接请求,接收完成三次握手的客户端 参数: sockfd:监听套接字文件描述符 addr:存放连接端的地址信息空间首地址 addrlen:想要接收的地址的长度, 调用后, 存放实际接收到的地址的长度 返回值: 成功返回新的文件描述符,通讯套接字 失败返回-1注意:阻塞等待客户端建立连接
练习1:使用TCP实现全双工聊天的功能
//clientA.c #include "head.h" int sockfd = 0; pthread_t tid_send; pthread_t tid_recv; void *sendfun(void *arg) { char tmpbuff[4096] = {0}; ssize_t nret = 0; while (1) { memset(tmpbuff, 0, sizeof(tmpbuff)); gets(tmpbuff); nret = send(sockfd, tmpbuff, strlen(tmpbuff), 0); if (-1 == nret) { perror("fail to send"); return NULL; } if (0 == strcmp(tmpbuff, ".quit")) { break; } } pthread_cancel(tid_recv); return NULL; } void *recvfun(void *arg) { char tmpbuff[4096] = {0}; ssize_t nret = 0; while (1) { memset(tmpbuff, 0, sizeof(tmpbuff)); nret = recv(sockfd, tmpbuff, sizeof(tmpbuff), 0); if (-1 == nret) { perror("fail to recv"); return NULL; } else if (0 == nret) { break; } if (0 == strcmp(tmpbuff, ".quit")) { break; } printf("RECV:%s\n", tmpbuff); } pthread_cancel(tid_send); return NULL; } int CreateTcpConnection(const char *pIp, int Port) { int ret = 0; struct sockaddr_in seraddr; sockfd = socket(AF_INET, SOCK_STREAM, 0); if (-1 == sockfd) { perror("fail to socket"); return -1; } seraddr.sin_family = AF_INET; seraddr.sin_port = htons(Port); seraddr.sin_addr.s_addr = inet_addr(pIp); ret = connect(sockfd, (struct sockaddr *)&seraddr, sizeof(seraddr)); if (-1 == ret) { perror("fail to connect"); return -1; } return sockfd; } int main(void) { int sockfd = 0; sockfd = CreateTcpConnection("192.168.0.138", 50000); pthread_create(&tid_send, NULL, sendfun, NULL); pthread_create(&tid_recv, NULL, recvfun, NULL); pthread_join(tid_send, NULL); pthread_join(tid_recv, NULL); close(sockfd); return 0; }//clientB.c #include "head.h" pthread_t tid_send; pthread_t tid_recv; int confd = 0; void *sendfun(void *arg) { char tmpbuff[4096] = {0}; ssize_t nret = 0; while (1) { memset(tmpbuff, 0, sizeof(tmpbuff)); gets(tmpbuff); nret = send(confd, tmpbuff, strlen(tmpbuff), 0); if (-1 == nret) { perror("fail to send"); return NULL; } if (0 == strcmp(tmpbuff, ".quit")) { break; } } pthread_cancel(tid_recv); return NULL; } void *recvfun(void *arg) { char tmpbuff[4096] = {0}; ssize_t nret = 0; while (1) { memset(tmpbuff, 0, sizeof(tmpbuff)); nret = recv(confd, tmpbuff, sizeof(tmpbuff), 0); if (-1 == nret) { perror("fail to recv"); return NULL; } else if (0 == nret) { break; } if (0 == strcmp(tmpbuff, ".quit")) { break; } printf("RECV:%s\n", tmpbuff); } pthread_cancel(tid_send); return NULL; } int CreateListenSocket(const char *pIp, int Port) { int ret = 0; int sockfd = 0; struct sockaddr_in seraddr; sockfd = socket(AF_INET, SOCK_STREAM, 0); if (-1 == sockfd) { perror("fail to socket"); return -1; } seraddr.sin_family = AF_INET; seraddr.sin_port = htons(Port); seraddr.sin_addr.s_addr = inet_addr(pIp); ret = bind(sockfd, (struct sockaddr *)&seraddr, sizeof(seraddr)); if (-1 == ret) { perror("fail to bind"); return -1; } ret = listen(sockfd, 10); if (-1 == ret) { perror("fail to listen"); return -1; } return sockfd; } int main(void) { int sockfd = 0; sockfd = CreateListenSocket("192.168.0.138", 50000); confd = accept(sockfd, NULL, NULL); if (-1 == confd) { perror("fail to accept"); return -1; } pthread_create(&tid_send, NULL, sendfun, NULL); pthread_create(&tid_recv, NULL, recvfun, NULL); pthread_join(tid_send, NULL); pthread_join(tid_recv, NULL); close(confd); close(sockfd); return 0; }练习2:使用TCP编写发送端和接收端,实现文件的发送
//send.c #include "head.h" int CreateTcpConnection(const char *pIp, int Port) { int sockfd = 0; int ret = 0; struct sockaddr_in seraddr; sockfd = socket(AF_INET, SOCK_STREAM, 0); if (-1 == sockfd) { perror("fail to socket"); return -1; } seraddr.sin_family = AF_INET; seraddr.sin_port = htons(Port); seraddr.sin_addr.s_addr = inet_addr(pIp); ret = connect(sockfd, (struct sockaddr *)&seraddr, sizeof(seraddr)); if (-1 == ret) { perror("fail to connect"); return -1; } return sockfd; } int main(int argc, char *argv[]) { int sockfd = 0; ssize_t nret = 0; int fd = 0; char tmpbuff[4096] = {0}; if (argc != 2) { fprintf(stderr, "Usage:./a.out filename\n"); return -1; } sockfd = CreateTcpConnection("192.168.0.138", 50000); sprintf(tmpbuff, "%s\r\n", argv[1]); nret = send(sockfd, tmpbuff, strlen(tmpbuff), 0); if (-1 == nret) { perror("fail to send"); return -1; } fd = open(argv[1], O_RDONLY); if (-1 == fd) { perror("fail to open"); return -1; } while (1) { nret = read(fd, tmpbuff, sizeof(tmpbuff)); if (nret <= 0) { break; } nret = send(sockfd, tmpbuff, nret, 0); if (-1 == nret) { perror("fail to send"); return -1; } } close(fd); close(sockfd); return 0; }//recv.c #include "head.h" int CreateListenSocket(const char *pIp, int Port) { int ret = 0; int sockfd = 0; struct sockaddr_in seraddr; sockfd = socket(AF_INET, SOCK_STREAM, 0); if (-1 == sockfd) { perror("fail to socket"); return -1; } seraddr.sin_family = AF_INET; seraddr.sin_port = htons(Port); seraddr.sin_addr.s_addr = inet_addr(pIp); ret = bind(sockfd, (struct sockaddr *)&seraddr, sizeof(seraddr)); if (-1 == ret) { perror("fail to bind"); return -1; } ret = listen(sockfd, 10); if (-1 == ret) { perror("fail to listen"); return -1; } return sockfd; } int main(void) { int sockfd = 0; int confd = 0; char tmpbuff[4096] = {0}; ssize_t nret = 0; int fd = 0; char *ptmp = NULL; sockfd = CreateListenSocket("192.168.0.138", 50000); confd = accept(sockfd, NULL, NULL); if (-1 == confd) { perror("fail to accept"); return -1; } nret = recv(confd, tmpbuff, sizeof(tmpbuff), 0); if (-1 == nret) { perror("fail to recv"); return -1; } ptmp = strstr(tmpbuff, "\r\n"); *ptmp = '\0'; fd = open(tmpbuff, O_WRONLY | O_CREAT | O_TRUNC, 0664); if (-1 == fd) { perror("fail to open"); return -1; } ptmp += 2; write(fd, ptmp, nret - (ptmp - tmpbuff)); while (1) { nret = recv(confd, tmpbuff, sizeof(tmpbuff), 0); if (-1 == nret) { perror("fail to recv"); return -1; } else if (0 == nret) { break; } write(fd, tmpbuff, nret); } close(fd); close(confd); close(sockfd); return 0; }//head.h #ifndef __HEAD_H__ #define __HEAD_H__ #include <stdio.h> #include <stdlib.h> #include <string.h> #include <sys/types.h> #include <sys/stat.h> #include <sys/wait.h> #include <unistd.h> #include <fcntl.h> #include <time.h> #include <dirent.h> #include <pthread.h> #include <semaphore.h> #include <signal.h> #include <sys/ipc.h> #include <sys/msg.h> #include <sys/shm.h> #include <sys/sem.h> #include <sys/socket.h> #include <netinet/in.h> #include <netinet/ip.h> #include <arpa/inet.h> #endif
三、TCP粘包问题
① 粘包:接收端应用层收到的数据多包数据粘连在一起。
② 原因:
- 由于发送和接收速率不匹配,导致接收缓冲区缓存了多包数据,应用层读取时,一次性读出;
- TCP发送端发送数据时将多包数据重新组包,数据会发生粘连,一起发送给接收端
③ 解决粘包问题:
(1) 加延时
(2)设置数据边界,每包数据增加分隔符,应用层根据分隔符解析(文件名\r\n)
(3)定长发送(4)带有帧头帧尾的自定义协议,应用层根据协议精准解析
数据帧结构:帧头 + 有效数据长度 + 数据 + 校验和 + 帧尾
常用帧头和帧尾:AA/BB 7E/ 5A与A5 EB90(航天常用)
常用校验方式:8位和校验(将有效数据累加,只保留低八位)16和校验(将有效数据累加,保留高低八位,两字节)
CRC校验
四、TCP报文头部
① 当ACK置1时, 32位确认序号才能正常使用
② 标志位介绍:FIN:结束通信
SYN:请求应答
RST:重新建立连接
PSH:发送数据
ACK:确认应答
URG:数据加急
③ 发送过程中保证数据传输:
TCP通过序号和确认号保证数据传输的完整性。
序号:该包数据起始字节的编号(本次发送的序号一般为上次接收到的确认号)。
确认序号:接收方希望发送方下次发送数据首字节的序号(本次发送的确认序号一般为上次接收到的序号 + 实际接收的长度)。
④ 滑动窗口机制:
保证传输过程中对流量进行控制。
通过调节TCP包头中的窗口大小来设置发送窗口和接口窗口大小,完成对流量及发送速率的控制。
⑤ 拥塞控制机制:
如果网络拥堵,带宽占用较大, TCP自带拥塞控制机制来实现数据可靠通信。
⑥ 掉线检测机制:
TCP三次握手建立后,四次挥手结束前,整个过程通信双方保持连接,可以通过掉线检测机制检测TCP连接是否保持。
五、TCP和UDP的区别
对比维度 TCP (传输控制协议) UDP (用户数据报协议) 连接性 面向连接
通信前必须通过三次握手建立连接,通信结束后四次挥手释放连接。无连接
无需建立连接,直接发送数据,类似发短信或寄信。可靠性 安全可靠
具备确认应答、超时重传、序号排序、流量控制和拥塞控制机制,保证数据无差错、不丢失、不重复、按序到达。不安全不可靠
尽最大努力交付,不保证数据是否到达、是否按序到达。若网络丢包或出错,UDP 本身不纠正。机制复杂度 实现机制复杂
需要维护连接状态、计算校验和、管理缓冲区、实现复杂的拥塞控制算法(如慢启动、快重传)。实现机制简单
仅包含端口号、长度和校验和字段,甚至校验和也可选,无复杂的逻辑控制。资源开销 资源开销大
每个连接需占用内核内存存储状态信息,首部开销大(20字节),且需维持连接的心跳包。资源开销小
无需维护连接状态,首部开销小(仅8字节),CPU 和内存占用极低。数据形式 字节流
数据无边界,发送方写入的多次数据可能在接收方合并为一次读取,即“粘包”现象。数据报
数据有边界,应用层交下来的报文,UDP 直接打包发送,接收方一次接收一个完整报文。传输效率 相对较慢
由于握手、确认机制和拥塞控制的介入,实时性较差,头部开销大,传输效率低。高效快速
无握手、无确认,无拥塞控制,即使网络拥塞也不降低发送速率,实时性极强。首部大小 20 字节 (最少) 8 字节 (固定) 适用场景 文件传输 (FTP)、网页浏览 (HTTP/HTTPS)、邮件 (SMTP/POP3)、远程登录 (SSH/Telnet) ——对数据完整性要求极高的场景。 视频直播、语音通话 (VoIP)、在线游戏、DNS 查询、广播/组播 ——对实时性敏感,允许少量丢包的场景。