c/s 模型 client server 客户端 /服务器
b/s模型 browser server 浏览器 /服务器
TCP是传输层的协议
TCP头部格式
- 源/目的端口:表示数据从哪个进程发送,发送到哪个进程去
- 序号:发送端发送数据包的编号
- 确认号:已经确认接收到的数据的编号(只有当ACK为1时,确认号才有用)
-
URG:紧急指针是否有效
ACK:确认号是否有效
PSH:提示接收端应用程序立刻从TCP缓冲区把数据读走
RST:对方要求重新建立连接,把携带RST标识的称为复位报文段
SYN:请求建立连接,把携带SYN标识的称为同步报文段
FIN:通知对方,要关闭连接了,把携带FIN标识的称为结束报文 - 16位窗口大小:进行流量窗口控制
- 16位校验和:检验数据是否一致
- 16位紧急指针:标识哪部分数据是紧急数据
TCP 粘包
发送方连续发送的多个数据包在接收方被合并成了一个数据包,或者一个大数据包被分片后接收方收到了分片的顺序混乱,导致数据边界模糊不清,接收方无法判断数据包的完整性和边界。
发生原因:
1. 发送方连续发送:如果发送方在短时间内连续发送多个数据包,而网络层或TCP层的拥塞控制机制导致这些数据包在底层被合并成一个更大的数据包发送,接收方就会收到一个包含了多个数据包内容的大包。
2. 接收方处理速度慢:如果接收方处理数据的速度慢于数据到达的速度,那么在接收方缓冲区中可能会积累多个数据包,等到接收方开始处理时,这些数据包就被一起读取了。
3. TCP滑动窗口机制:TCP使用滑动窗口来控制流量,当窗口大小调整时,可能会导致数据包的合并或分片。
解决办法:
1. 固定长度消息:发送固定长度的消息,接收方根据固定的长度来分割数据包,这种方法简单但不灵活,且可能造成空间浪费。
2. 使用消息头:自定义协议,在消息前添加一个消息头,包含消息的总长度等信息,接收方先读取消息头,根据消息头的长度来读取后续的消息内容。
3. 特殊分隔符:在消息之间插入特殊的分隔符或分隔消息,接收方根据分隔符来区分不同的消息。
4. 心跳包或空闲检测:定时发送心跳包或检测空闲时间,如果超过一定时间没有数据发送,则发送一个空消息,这样可以保证数据包的边界清晰。
5. 复合协议:在应用层设计更复杂的协议,如HTTP/2或QUIC,它们在设计时就已经考虑到了数据包的边界问题,可以有效避免粘包现象。
TCP滑动窗口机制
在没有接收到上次发送数据的ACK,还可以发送下一个数据报,可以通过后面的ACK来确认前面发送的数据报也称为累计确认.
没有窗口的情况下(一发一应的形式) :我给朋友发微信,我只能一直等待它给我回复信息,这样我等待的时间不能做其他的事情只能等待---降低了通信效率
有窗口的情况下 : 我给朋友发微信,我一次把我想说的话全部给他发过去,然后我先做其他的事情,等到不忙了在看他有没有回复信息,这样我可以在等待的时间做其他的事情,大大的提高通信效率.
跟聊天窗口一样
主要目的是防止发送方过快地发送数据,导致接收方缓冲区溢出或网络拥塞。通过动态调整窗口大小,可以有效地控制数据传输速率,提高网络资源的利用率,同时确保数据的可靠传输。
bind:Address already in use
这通常意味着已经有另一个进程在使用你试图绑定的同一个IP地址和端口号。在TCP/IP协议中,每个监听的套接字(socket)必须有唯一的地址对(IP address, port number)。
TCP为什么安全可靠:
1.在通信前建立三次握手连接
SYN
SYN+ACK
ACK
2.在通信过程中通过序列号和确认号保障数据传输的完整性
本次发送序列号:上次收到的确认号
本次发送确认号:上次接收到的序列号 + 实际接收的数据长度
在传输过程中使用滑动窗口实现流量控制
3.在通信结束时使用四次挥手结束连接保障数据传输的完整性
UDP和TCP的区别:
1.UDP和TCP都是传输层的协议
2.UDP实现机制简单、资源开销小、不安全不可靠
3.TCP实现机制复杂、资源开销大、安全可靠
4.UDP是无连接的、TCP有连接的、UDP是以数据包形式传输、TCP是以流的方式传输
TCP通信实现流程
需要用到的接口
TCP发端:
1.socket():
创建一个套接字(socket descriptor),用于网络通信。通常使用如下代码:
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
其中,AF_INET 表示使用IPv4协议族,SOCK_STREAM 表示使用面向连接的TCP协议。
2.connect():
尝试与服务端建立连接。需要提供服务端的IP地址和端口号。例如:struct sockaddr_in server_addr;
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(SERVER_PORT)
int connect(int sockfd, const struct sockaddr *addr,
socklen_t addrlen);
功能:
发送链接请求
参数:
sockfd:套接字文件描述符
addr:目的地址存放空间首地址
addrlen:IP地址的大小
返回值:
成功返回0
失败返回-1
3.send ():
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
功能:
发送数据
参数:
sockfd:文件描述符
buf:发送数据空间首地址
len:发送数据的长度
flags:属性默认为0
返回值:
成功返回实际发送字节数
失败返回-1
recv
4.recv():
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
功能:
接收数据
参数:
sockfd:套接字文件描述符
buf:存放数据空间首地址
len:最大接收数据的长度
flags:属性默认为0
返回值:
成功返回实际接收字节数
失败返回-1
如果对方退出,返回0
5.close ():
close(sockfd);
销毁套接字文件描述符
int CreateTcpClient(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 = connect(sockfd, (struct sockaddr *)&seraddr, sizeof(seraddr));
if (-1 == ret)
{
perror("fail to connect");
return -1;
}
return sockfd;
}
struct sockaddr_in seraddr;
struct sockaddr_in seraddr;是C语言中声明一个 struct sockaddr_in 类型变量 seraddr的语句。
struct sockaddr_in是一个用于存储Internet(IPv4)协议地址信息的标准结构体,广泛应用于网络编程,特别是在使用TCP或UDP套接字时。
struct sockaddr_in结构体通常包含以下几个成员:
struct sockaddr_in {
sa_family_t sin_family; /* address family: AF_INET */
in_port_t sin_port; /* port in network byte order */
struct in_addr sin_addr; /* internet address */
};•sa_family_t sin_family: 存储地址家族(Address Family),对于IPv4地址,应设置为 AF_INET。
•in_port_t sin_port: 存储端口号,以网络字节序(Big-Endian)表示。
通常使用 htons() 函数将主机字节序(Little-Endian)的端口号转换为网络字节序。
•struct in_addr sin_addr: 用于存储IPv4地址。
struct in_addr 是一个嵌套在 struct sockaddr_in 中的结构体,其定义如下:
struct in_addr {
uint32_t s_addr; /* address in network byte order */
};uint32_t s_addr 用于存储IPv4地址,同样以网络字节序表示。通常使用 inet_addr() 或 inet_aton() 函数将点分十进制的IPv4地址字符串转换为网络字节序的整数形式,然后赋值给 sin_addr.s_addr。
其中:
在实际使用中,struct sockaddr_in
通常用于以下场景:
例如,创建一个表示服务器监听地址的 struct sockaddr_in变量:
struct sockaddr_in seraddr;
seraddr.sin_family = AF_INET; // IPv4地址家族
seraddr.sin_port = htons(SERVER_PORT); // 将主机字节序端口号转换为网络字节序
seraddr.sin_addr.s_addr = inet_addr(pip); // 将点分十进制IP地址字符串转换为网络字节序整数
// 然后可以将 seraddr 传递给 bind() 函数
bind(sockfd, (struct sockaddr *)&seraddr, sizeof(seraddr));
TCP收端:
1. socket
2. bind
绑定套接字到本地的IP地址和端口,以便客户端能够找到并连接。
int bind(int sockfd, const struct sockaddr *addr,socklen_t addrlen);
功能:
在套接字上绑定一个IP地址和端口号
参数:
sockfd:套接字文件描述符
addr:绑定IP地址空间首地址
addrlen:绑定IP地址的长度
返回值:
成功返回0
失败返回-1
3. listen
int listen(int sockfd, int backlog);
功能:
监听客户端发送的连接请求
该函数不会阻塞
参数:
sockfd:套接字文件描述符
backlog:允许等待的尚未被处理的三次握手请求的最大个数
返回值:
成功返回0
失败返回-1
4. accept
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
功能:
处理等待连接队列中的第一个连接请求
该函数具有阻塞功能(如果没有人发送链接请求,会阻塞等待)
参数:
socket:套接字文件描述符
address:存放IP地址的空间首地址
addrlen:存放IP地址大小空间首地址
返回值:
成功返回一个新的文件描述符
失败返回-1
5. send
6. recv
7.close
TCP通信 写一个客户端和服务端两者建立通信
/*************************************************************************
> File Name: head.h
> Author: yas
> Mail: rage_yas@hotmail.com
> Created Time: 2024年04月01日 星期一 20时19分06秒
************************************************************************/
#ifndef _HEAD_H
#define _HEAD_H
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <unistd.h>
#include <fcntl.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <dirent.h>
#include <time.h>
#include <pwd.h>
#include <grp.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 <arpa/inet.h>
#include <netinet/ip.h>
#include <netinet/in.h>
#endif
服务端
/*************************************************************************
> File Name: recv.c
> Author: yas
> Mail: rage_yas@hotmail.com
> Created Time: 2024年04月01日 星期一 22时58分52秒
************************************************************************/
#include"head.h"
int main(void)
{
int ret = 0;
int sockfd = 0;
int confd = 0;
char tmpbuff[1024] = {0};
ssize_t nsize = 0;
struct sockaddr_in seraddr;
seraddr.sin_family = AF_INET;
seraddr.sin_port = htons(50000);
seraddr.sin_addr.s_addr = INADDR_ANY;//允许任何ip地址连接
sockfd = socket(AF_INET,SOCK_STREAM,0);//创建一个基于IPV4的tcp套接字
if(-1 == sockfd)
{
perror("fail to socket");
return -1;
}
ret = bind(sockfd,(struct sockaddr *)&seraddr,sizeof(seraddr)); //将创建的套接字绑定到指定的服务器地址 seraddr
if(-1 == ret)
{
perror("fail to bind");
return -1;
}
ret = listen(sockfd,10);//将套接字设置为监听模式 允许10个客户端连接请求排队
if(-1 == ret)
{
perror("fail to listen");
return -1;
}
confd = accept(sockfd,NULL,NULL);//接受一个客户端的连接请求 返回一个新的套接字文件描述符confd 用于与该客户端通信
if(-1 == confd)
{
perror("fail to accept");
return -1;
}
nsize = recv(confd,tmpbuff,sizeof(tmpbuff),0); //接收客户端数据
if(-1 == nsize)
{
perror("fail to recv");
return -1;
}
printf("RECV:%s\n",tmpbuff);
memset(tmpbuff,0,sizeof(tmpbuff)); //清零 tmpbuff中的数据
fgets(tmpbuff,sizeof(tmpbuff),stdin); //会有输入阻塞 等待用户输入
tmpbuff[strcspn(tmpbuff,"\n")] ='\0'; // 消除 由fgets 自动产生的末尾的换行符
nsize = send(confd,tmpbuff,strlen(tmpbuff),0);//等待用户输入并发送给客户端
if(-1 == nsize)
{
perror("fail to send");
return -1;
}
close(confd);
close(sockfd);
return 0;
}
客户端
/*************************************************************************
> File Name: send.c
> Author: yas
> Mail: rage_yas@hotmail.com
> Created Time: 2024年04月01日 星期一 20时25分08秒
************************************************************************/
/***
*客户端
*/
#include"head.h"
int main(void)
{
int ret = 0;
int sockfd = 0;
struct sockaddr_in seraddr;
char tmpbuff[1024] = {0};
ssize_t nsize =0;
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(50000);
seraddr.sin_addr.s_addr = inet_addr("192.168.1.151"); //本机的ip地址
/* 连接服务器*/
ret = connect(sockfd,(struct sockaddr*)&seraddr,sizeof(seraddr));//建立tcp连接
if(-1 == ret)
{
perror("fail to connect");
return -1;
}
/*等待用户输入并发送给服务器*/
gets(tmpbuff);
nsize = send(sockfd,tmpbuff,strlen(tmpbuff),0);//发送数据
if(-1 == nsize)
{
perror("fail to send");
return -1;
}
/*
*接收服务器数据
* */
memset(tmpbuff,0,sizeof(tmpbuff));
nsize = recv(sockfd,tmpbuff,sizeof(tmpbuff),0);//接收数据
if(-1 == nsize)
{
perror("fail to recv");
return -1;
}
printf("RECV:%s\n",tmpbuff);
close(sockfd);
return 0;
}
实现效果
通过TCP实现文件发送接收
发送端
/*************************************************************************
> File Name: send.c
> Author: yas
> Mail: rage_yas@hotmail.com
> Created Time: 2024年04月01日 星期一 23时43分33秒
************************************************************************/
#include"head.h"
int main(void)
{
int ret = 0;
int sockfd =0;
struct sockaddr_in recvaddr;
char tmpbuff[1024] = {0};
ssize_t nsize = 0;
ssize_t nret = 0;
int fd = 0;
printf("Enter filename:\n");
gets(tmpbuff);
sockfd = socket(AF_INET,SOCK_STREAM,0);
if(-1 == sockfd)
{
perror("fail to socket");
return -1;
}
recvaddr.sin_family = AF_INET;
recvaddr.sin_port = htons(50000);
recvaddr.sin_addr.s_addr = inet_addr("192.168.1.151");
ret = connect(sockfd,(struct sockaddr*)&recvaddr,sizeof(recvaddr));
if(-1 == ret)
{
perror("fail to connect");
return -1;
}
nsize = send(sockfd,tmpbuff,strlen(tmpbuff),0);
if(-1 == nsize)
{
perror("fail to send");
return -1;
}
sleep(1);
//打开文件
fd = open(tmpbuff,O_RDONLY);
if(-1 == fd)
{
perror("fail to open");
return -1;
}
//循环读取数据并发送
while(1)
{
memset(tmpbuff,0,sizeof(tmpbuff));//确保读取之前tmpbuff 已经清零
nret = read(fd,tmpbuff,sizeof(tmpbuff));// 使用read读取 tmpbuff大小(1024)字节的数据
if(nret<0)
{
break;
}
nsize = send(sockfd,tmpbuff,nret,0);
if(-1 == nsize)
{
perror("fail to send");
return -1;
}
}
close(fd);
close(sockfd);
}
接收端
/*************************************************************************
> File Name: recv.c
> Author: yas
> Mail: rage_yas@hotmail.com
> Created Time: 2024年04月02日 星期二 00时01分51秒
************************************************************************/
#include"head.h"
int main(void)
{
int ret = 0;
int sockfd = 0;
int confd = 0;
ssize_t nsize =0;
struct sockaddr_in recvaddr;
char tmpbuff[1024] = {0};
int fd = 0;
sockfd = socket(AF_INET,SOCK_STREAM,0);
if(-1 == sockfd)
{
perror("fail to socket");
return -1;
}
recvaddr.sin_family = AF_INET;
recvaddr.sin_port = htons(50000);
recvaddr.sin_addr.s_addr = INADDR_ANY;
ret = bind(sockfd,(struct sockaddr*)&recvaddr,sizeof(recvaddr));
if(-1 == ret)
{
perror("fail to bind");
return -1;
}
ret = listen(sockfd,10);
if(-1 == ret)
{
perror("fail to listen");
return -1;
}
confd = accept(sockfd,NULL,NULL);
if(-1 == confd)
perror("fail to accept");
return -1;
}
nsize = recv(confd,tmpbuff,sizeof(tmpbuff),0);
if(-1 == nsize)
{
perror("fail to recv");
return -1;
}
//打开文件 如果文件不存在则创建 如果文件之前存在 则清空 ,写入模式
fd = open(tmpbuff,O_WRONLY|O_CREAT|O_TRUNC,0664);
if(-1 == fd)
{
perror("fail to open");
return -1;
}
//循环接收数据并写入文件
while(1)
{
nsize = recv(confd,tmpbuff,sizeof(tmpbuff),0);
if(-1 == nsize)
{
perror("fail to recv");
return -1;
}
else if(0 == nsize)
{
break;
}
write(fd,tmpbuff,nsize);
}
close(fd);
close(confd);
close(sockfd);
return 0;
}