欢迎光临
我们一直在努力

Linux网络编程02-TCP协议

一、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 查询、广播/组播 —— 对实时性敏感,允许少量丢包的场景。
赞(0)
未经允许不得转载:171主机测评 » Linux网络编程02-TCP协议
分享到: 更多 (0)

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址