7.2 网络编程_day5
一.服务器模型
【1】 循环服务器
特点: 循环服务器,同一时刻,服务器只能连接一个客户端请求;
框架:
TCP服务器流程:
socket()
bind();
listen()
while(1)
{
accept()
while(1)
{
recv();
}
close(accpetfd);
}
close(sockfd);
UDP服务器流程:
socket();
bind();
while(1)
{
recvfrom();
}
close(sockfd);
TCP全双工通信:
./server:
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <pthread.h>
#include <signal.h>
#include <sys/wait.h>
int main(int argc, char const *argv[])
{
if(argc != 2)
{
printf("please input %s <port>\n",argv[0]);
return -1;
}
//1.创建流式套接字
int sockfd = socket(AF_INET,SOCK_STREAM,0);
if(sockfd < 0)
{
perror("socket err:");
return -1;
}
printf("socket is ok\n");
//2.填充ipv4结构体
struct sockaddr_in saddr,caddr;
saddr.sin_family = AF_INET;
saddr.sin_port = htons(atoi(argv[1]));
saddr.sin_addr.s_addr = inet_addr("0.0.0.0");
int len = sizeof(caddr);
//绑定套接字
if(bind(sockfd,(struct sockaddr *)&saddr,sizeof(saddr)) < 0)
{
perror("bind err:");
return -1;
}
printf("bind is ok\n");
//3/监听
if(listen(sockfd,5) < 0)
{
perror("listen is err:");
return -1;
}
printf("listen is ok\n");
while(1)
{
//阻塞等待客户端链接
int acceptfd = accept(sockfd,(struct sockaddr *)&caddr,&len);
if(acceptfd < 0)
{
perror("accept is err:");
return -1;
}
printf("client ip: %s port %d\n",inet_ntoa(caddr.sin_addr),ntohs(caddr.sin_port));
//创建进程
pid_t pid = fork();
if(pid < 0)
{
perror("fork is err:");
return -1;
}
else if(pid == 0) //子进程
{
//循环发送
char buf[128];
while(1)
{
fgets(buf,sizeof(buf),stdin);
if(buf[strlen(buf)-1] == '\n')
buf[strlen(buf)-1] = '\0';
send(acceptfd,buf,sizeof(buf),0);
}
//exit(0);
}
else //父进程
{
//循环接受
char buf[128];
while(1)
{
int revbyte = recv(acceptfd,buf,sizeof(buf),0);
if(revbyte < 0)
{
perror("recv is err:");
return -1;
}
else if(revbyte == 0)
{
printf("client is exit\n");
kill(pid,SIGINT); //终止子进程运行
wait(NULL);
break;
}
else
{
printf("%s\n",buf);
}
}
}
}
close(sockfd);
return 0;
}
./client:
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <pthread.h>
void *handler(void * arg)
{
int sockfd = *((int *)arg);
char buf[128];
while(1)
{
//接受
int recvbyte = recv(sockfd,buf,sizeof(buf),0);
if(recvbyte < 0)
{
perror("recv is err:");
return NULL;
}
else
{
printf("%s\n",buf);
}
}
}
int main(int argc, char const *argv[])
{
if(argc != 3)
{
printf("please input %s <port> < ip>\n",argv[0]);
}
//socket
int sockfd = socket(AF_INET,SOCK_STREAM,0);
if(sockfd < 0)
{
perror("sockfd is err:");
return -1;
}
//填充结构体
struct sockaddr_in caddr;
caddr.sin_family = AF_INET;
caddr.sin_port = htons(atoi(argv[2]));
caddr.sin_addr.s_addr = inet_addr(argv[1]);
//链接
if(connect(sockfd,(struct sockaddr *)&caddr,sizeof(caddr)) < 0)
{
perror("connect is err:");
return -1;
}
//1.创建线程ID
pthread_t tid;
pthread_create(&tid,NULL,handler,&sockfd);
pthread_detach(tid);
char buf[128];
while(1)
{
//发送
fgets(buf,sizeof(buf),stdin);
if(buf[strlen(buf)-1] == '\n')
buf[strlen(buf)-1] = '\0';
send(sockfd,buf,sizeof(buf),0);
if(strncmp(buf,"quit",4) == 0)
{
break;
}
}
close(sockfd);
return 0;
}
【2】 并发服务器
特点: 同一时刻可以相应多个客户端的请求 (同时连接多个客户端,并能够通信)
1.多进程/多线程实现并发
每来一个客户端连接, 开一个进程/线程来专门处理客户端的数据, 实现简单, 资源占用大;
框架:
socket()
bind();
listen();
while(1)
{
accept();
if(fork() == 0) //子进程
{
while(1)
{
process();
}
close(client_fd);
exit();
}
else
{
}
}
函数接口:
创建线程 pthread_create
退出线程: pthread_exit(void *retval)
将线程设置为游离态,自动回收线程资源, pthread_detach
2.IO多路复用
特点: 借助select,poll,epoll机制, 将新连接的客户端描述符增加到描述符表中,只需要一个线程即可处理所有的客户端连接,在嵌入式开发中应用广泛,但代码复杂度较高。
并发服务器总结:
多进程:
优点: 服务器更稳定 , 父子进程资源独立, 安全性高一点
缺点: 需要开辟多个进程,大量消耗资源
多线程:
优点: 相对多进程, 资源开销小, 线程共享同一个进程的资源
缺点: 需要开辟多个线程,而且线程安全性较差
IO多路复用:
优点: 节省资源, 减小系统开销,性能高;
缺点: 代码复杂性高
【1】广播和组播 UDP
一.广播 (UDP协议)
广播地址: 主机号最大的地址;
以192.168.1.0 (255.255.255.0) 网段为例,最大的主机地址192.168.1.255代表该网段的广播地址
●前面介绍的数据包发送方式只有一个接受方,称为单播
●如果同时发给局域网中的所有主机,称为广播
(同一局域网内的主机都会接收到,如果其他主机没有加入广播站,就会将消息丢弃)
●只有用户数据报(使用UDP协议)套接字才能广播
●一般被设计为局域网搜索协议
广播代码流程,主要基于 setsockopt:
setsockopt 设置套接字的属性
头文件:
#include<sys.socket.h>
#include<sys/types.h>
#include<sys/time.h>
原型: int setsockopt(int sockfd,int level,int optname,\
void *optval,socklen_t optlen)
功能: 获得/设置套接字属性
参数:
sockfd:套接字描述符
level:协议层
optname:选项名
optval:选项值
optlen:选项值大小
返回值: 成功 0 失败-1
socket属性
选项名称 说明 数据类型
==== SOL_SOCKET 应用层 ====
SO_BROADCAST 允许发送广播数据 int
SO_DEBUG 允许调试 int
SO_DONTROUTE 不查找路由 int
SO_ERROR 获得套接字错误 int
SO_KEEPALIVE 保持连接 int
SO_LINGER 延迟关闭连接 struct linger
SO_OOBINLINE 带外数据放入正常数据流 int
SO_RCVBUF 接收缓冲区大小 int
SO_SNDBUF 发送缓冲区大小 int
SO_RCVLOWAT 接收缓冲区下限 int
SO_SNDLOWAT 发送缓冲区下限 int
SO_RCVTIMEO 接收超时 struct timeval
SO_SNDTIMEO 发送超时 struct timeval
SO_REUSEADDR 允许重用本地地址和端口 int
SO_TYPE 获得套接字类型 int
SO_BSDCOMPAT 与BSD系统兼容 int
==== IPPROTO_IP 网络层 ====
IP_HDRINCL 在数据包中包含IP首部 int
IP_OPTINOS IP首部选项 int
IP_TOS 服务类型 int
IP_TTL 生存时间 int
IP_ADD_MEMBERSHIP 将指定的IP加入多播组 struct ip_mreq
==== IPPRO_TCP 传输层 ====
TCP_MAXSEG TCP最大数据段的大小 int
TCP_NODELAY 不使用Nagle算法 int
端口与地址复用:
int opt = 1;
setsockopt(sockfd,SOL_SOCKET,SO_REUSEADDR,&opt,sizeof(opt));
广播的发送者:
- 创建用户数据报套接字;
sockfd = socket(AF_INET,SOCK_DGRAM,0);
2.setsockopt可以设置套接字属性,先设定该套接字允许发送广播
int optval = 1;
// SOL_SOCKET 传输层 SO_BROADCAST 允许发送广播
setsockopt(sockfd,SOL_SOCKET,SO_BROADCAST,&optval,sizeo(optval));
3.接收方地址指定为广播地址,指定端口号
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(atoi(argv[2]));
addr.sin_addr.s_addr = inet_addr(argv[1]);
- 发送数据包
sendto(sockfd,buf,sizeof(buf),0,\
(struct sockaddr*)&addr,sizeof(addr));
发送者需要借助 setsockopt 开通广播权限:
广播的接收者:
基本无需改动
-
创建用户数据报套接字
sockfd = socket(AF_INET,SOCK_DGRAM,0); -
绑定IP地址(广播IP或0.0.0.0)和端口
广播的接收者需要加入 广播站
struct sockaddr_in saddr,caddr;
saddr.sin_family = AF_INET;
saddr.sin_port = htons(atoi(argv[2]));
saddr.sin_addr.s_addr = inet_addr(“0.0.0.0”);
//广播地址或0.0.0.0
//0.0.0.0 是一个特殊的IP地址,用于表示服务器端将监听所有可用的网络接口
// 而不仅仅是IP地址,广播地址也会监听。socklen_t len = sizeof(caddr);
bind(sockfd,(struct sockaddr *)&saddr,sizeof(saddr)); -
等待接收数据
recvfrom(sockfd,buf,sizeof(buf),0,
(struct sockaddr *)&caddr,&len);
广播的缺点:
广播方式发给所有的主机,过多的广播会大量的占用网络带宽,造成广播风暴,影响正常的通信
二.组播(多播)
。单播方式只能发给一个接收方
。 组播是一个人发送,加入到多播组的主机接收数据
。 多播方式既可以发给多个主机,又能避免像广播一样造成过多的负载:
组播的地址:
IP的二级划分中 D类IP:
第一字节的前四位固定为 1110
D类IP : 224.0.0.1 - 239.255.255.255
224.10.10.10
组播的接收者:
1.创建用户数据报套接字;
sockfd = socket(AF_INET,SOCK_DGRAM,0);
- 加入多播组 : 只有接收者加入多播组 (广播是发送者需要设置广播属性,组播是接收者加入多播组)
//2.将主机假如多播组
struct ip_mreq mreq; //组播的结构体变量
mreq.imr_multiaddr.s_addr = inet_addr(argv[1]); //组IP
mreq.imr_interface.s_addr = inet_addr("0.0.0.0"); //本地IP加入到上面的多播组
//设置套接字
//记得去表内查找 组播属于IPPROTO_IP网络层,IP_ADD_MEMBERSHIP组播 struct mreq 数据类型
//该函数接口将结构体内的本地IP,绑定到了组IP内
setsockopt(sockfd,IPPROTO_IP,IP_ADD_MEMBERSHIP,&mreq,sizeof(mreq));
- 绑定组播IP地址(绑定组播IP或0.0.0.0)和端口
//3. 填充结构体: 绑定组播ip和端口
struct sockaddr_in saddr,caddr;
saddr.sin_family = AF_INET;
saddr.sin_port = htons(atoi(argv[2]));
saddr.sin_addr.s_addr = inet_addr("0.0.0.0"); //组IP
- 接收数据包
recvfrom(sockfd,buf,sizeof(buf),0,(struct sockaddr *)&caddr,&len);
组播的发送者
- 创建用户数据报套接字;
sockfd = socket(AF_INET,SOCK_DGRAM,0);
- 指定接收方地址指定为组播地址 224.0.0.10 (224.0.0.1 - 239.255.255.255都可以) 并指定接收端端口信息
//2. 填充结构体: 组播ip 和 端口
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(atoi(argv[2]));
addr.sin_addr.s_addr = inet_addr(argv[1]); //组播的IP
- 发送数据包
sendto(sockfd,buf,sizeof(buf),0,(struct sockaddr *)&addr,sizeof(addr));
三. 本地套接字通信
特性:
· unix网络编程最开始有的编程都是一台主机内进程和进程之间的编程。(本地通信)
· socket同样可以用于本地间进程通信, 创建套接字时使用本地协议AF_LOCAL或AF_UNIX
· 分为流式套接字和数据报套接字
· 和其他进程间通信相比使用方便、效率更高,常用于前后台进程通信。
unix域套接字编程,实现本间进程的通信,依赖的是s类型的文件;(套接字文件)
TCP,UDP 都是依赖IP 端口号进行通信,实现两个主机通信
本地通信不需要ip和端口,所以无法进行两个主机通信
核心代码:
#include <sys/socket.h>
#include <sys/un.h>
struct sockaddr_un {
sa_family_t sun_family; /* 本地协议 AF_UNIX */
char sun_path[UNIX_PATH_MAX]; /* 本地路径 s类型的套接字文件 */
};
unix socket = socket(AF_UNIX, type, 0); //type为流式或数据包套接字
struct sockaddr_un myaddr;
myaddr.sun_family = AF_UNIX; //填充UNIX域套接字
strcpy(saddr.sun_path,"./myunix"); // 创建套接字的路径(将套接字创建到哪? 服务器与客户端一致)
相关代码:
./server:
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <dirent.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
void list_server(int acceptfd, char *buf, int size);
void put_server(int acceptfd, char *buf, int size);
void get_server(int acceptfd,char *buf, int size);
int main(int argc, char const *argv[])
{
if (argc != 2)
{
printf("please input %s <port>\n", argv[0]);
return -1;
}
//1.创建流式套接字
int sockfd = socket(AF_INET, SOCK_STREAM, 0); //链接
if (sockfd < 0)
{
perror("socket err.");
return -1;
}
printf("sockfd:%d\n", sockfd); //3
//填充ipv4的通信结构体
struct sockaddr_in saddr, caddr;
saddr.sin_family = AF_INET;
saddr.sin_port = htons(atoi(argv[1])); //"8888" int a= atoi("8888")//a=8888
//saddr.sin_addr.s_addr = inet_addr(argv[1]);
//设置服务器自动获取自己主机的ip
// saddr.sin_addr.s_addr = htonl(INADDR_ANY);//INADDR_ANY 0x00000000 "0.0.0.0"
// saddr.sin_addr.s_addr = INADDR_ANY;
saddr.sin_addr.s_addr = inet_addr("0.0.0.0");
socklen_t len = sizeof(caddr);
//2.绑定套接字 ip和端口(自己)
if (bind(sockfd, (struct sockaddr *)&saddr, sizeof(saddr)) < 0)
{
perror("bind err.");
return -1;
}
printf("bind ok.\n");
//3.监听
if (listen(sockfd, 5) < 0)
{
perror("listen err.");
return -1;
}
printf("listen ok.\n");
//4.阻塞等待客户端链接
while (1)
{
int acceptfd = accept(sockfd, (struct sockaddr *)&caddr, &len);
if (acceptfd < 0)
{
perror("accept err.");
return -1;
}
printf("acceptfd=%d\n", acceptfd); //通信
printf("client:ip=%s port=%d\n",
inet_ntoa(caddr.sin_addr), ntohs(caddr.sin_port));
//循环收发消息
char buf[128];
int ret;
while (1)
{
ret = recv(acceptfd, buf, sizeof(buf), 0);
if (ret < 0)
{
perror("recv err.");
return -1;
}
else if (ret == 0)
{
printf("client exit\n");
break;
}
else
{
if (strncmp(buf, "list", 4) == 0)
{ //打开当前目录读文件判断文件是普通文件将文件名传给客户端
list_server(acceptfd, buf, sizeof(buf));
}
else if (strncmp(buf, "put ", 4) == 0)
{ //接收文件(本地:打开新建文件接收客户端发送过来的内容写到文件)
put_server(acceptfd, buf, sizeof(buf));
}
else if (strncmp(buf, "get ", 4) == 0)
{ //发送文件
//(本地:打开文件读文件内容发送给客户端)
get_server(acceptfd, buf, sizeof(buf));
}
}
}
close(acceptfd);
}
close(sockfd);
return 0;
}
//3、get
void get_server(int acceptfd,char *buf, int size) //buf->put xxx.c
{
//1>打开文件 读
int fd = open(buf + 4, O_RDONLY);
if (fd < 0)
{
perror("open file err.");
return;
}
int ret; //实际读到的个数
while ((ret = read(fd, buf, size - 1)) != 0)
{
buf[ret] = '\0';
send(acceptfd, buf, size, 0);
}
strcpy(buf, "get ok.");
send(acceptfd, buf, size, 0);
}
//2.接收文件(本地:打开新建文件接收客户端发送过来的内容写到文件)
void put_server(int acceptfd, char *buf, int size)
{
int fd = open(buf + 4, O_TRUNC | O_CREAT | O_WRONLY, 0666);
if (fd < 0)
{
perror("open err.");
return;
}
while (1)
{
if (recv(acceptfd, buf, size, 0) < 0)
{
perror("recv err.");
return;
}
if (strncmp(buf, "put ok.", 7) == 0)
break;
write(fd, buf, strlen(buf));
}
}
//list:打开当前目录读文件判断文件是普通文件将文件名传给客户端
void list_server(int acceptfd, char *buf, int size)
{
//1.打开当前目录文件
DIR *dir = opendir("./");
if (dir == NULL)
{
perror("opendir err.");
return;
}
//2.循环读目录文件 readdir
struct dirent *dp = NULL;
struct stat st;
while ((dp = readdir(dir)) != NULL)
{
//dp->d_name拿到的文件名
//判断文件属性stat
stat(dp->d_name, &st);
if (S_ISREG(st.st_mode))
{
strcpy(buf, dp->d_name);
send(acceptfd, buf, size, 0);
}
}
//发送结束标志
strcpy(buf, "list ok.");
send(acceptfd, buf, size, 0);
}
./client:
#include <stdio.h>
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h> /* superset of previous */
#include <unistd.h>
#include <arpa/inet.h>
#include <string.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <sys/un.h>
int main(int argc, char const *argv[])
{
//1.创建套接字 - 返回建立连接的文件描述符
int sockfd = socket(AF_UNIX,SOCK_STREAM,0);
if(sockfd < 0)
{
perror("socket is err:");
return -1;
}
printf("sockfd: %d\n",sockfd);
//定义选项值
int op = 1;
//将socket套接字设置为 允许端口重用
setsockopt(sockfd,SOL_SOCKET,SO_REUSEADDR,&op,sizeof(op));
//2. 绑定套接字 - 填充结构体
struct sockaddr_un saddr;
saddr.sun_family = AF_UNIX;
strcpy(saddr.sun_path,"./fd");
connect(sockfd,(struct sockaddr *)&saddr,sizeof(saddr));
char buf[128];
while(1)
{
fgets(buf,sizeof(buf),stdin);
send(sockfd,buf,sizeof(buf),0);
}
close(sockfd);
return 0;
}
【1】wireshark抓包工具
一.抓包工具的使用:
- 安装
虚拟机 : sudo apt-get install wireshark
windows: 小飞机
注:抓包的过程,就是抓网卡流经的一些数据。启动时不加sudo找不到网卡,没有办法找到内容。
wireshark可以用来抓包,必须是流经网卡的包:
两台不同的主机通信 或 两台不同的操作系统(windows linux)之间 才可以进行抓包
服务器端代码运行在ubuntu
客户端代码运行在windows下
使用流程:
- 先运行服务器
- 打开TCP/UDPbg
- wireshark
抓包工具界面如下:
过滤其他无关的包:
客户端连接服务器,产生握手包:
发送消息:
客户端和服务器断开,产生挥手包 :
##【2】抓包工具与包头分析:
【3】网络协议头的分析 [了解]
【4】以太网完整帧格式:
对于网络层最大数据帧长度是1500字节(MTU: 最大传输单元)
对于链路层最大数据长度是1518字节(1500(网络层)+14(以太网)+4(CRC检错))
TCP粘包、拆包发生原因:
发生TCP粘包或拆包有很多原因,常见的几点:
1、要发送的数据大于TCP发送缓冲区剩余空间大小,将会发生拆包。
2、待发送数据大于MSS(传输层的最大报文长度),TCP在传输前将进行拆包。
3、要发送的数据小于TCP发送缓冲区的大小,TCP将多次写入缓冲区的数据一次发送出去,将会发生粘包。
4、接收数据端的应用层没有及时读取接收缓冲区中的数据,将发生粘包。
粘包、拆包解决办法:
解决问题的关键在于如何给每个数据包添加边界信息,常用的方法有如下:
1、发送端给每个数据包添加包首部,首部中应该至少包含数据包的长度,这样接收端在接收到数据后,
通过读取包首部的长度字段,便知道每一个数据包的实际长度了。
2、发送端将每个数据包封装为固定长度(不够的可以通过补0填充);
这样接收端每次从接收缓冲区中读取固定长度的数据就自然而然的把每个数据包拆分开来。
3、可以在数据包之间设置边界,如添加特殊符号,这样,接收端通过这个边界就可以将不同的数据包拆分开。
以太网头:
以太网中封装了 目的mac地址 以 及 源mac地址, 还有ip类型, 以太网又称之为mac头 -快递员
切换网络时, ip地址会改变, mac地址不会改变
0x0800 只接收发往本机的mac的ip类型的数据帧
0x0806 只接收发往本机的ARP类型的数据帧
ARP: ARP协议用于将IP地址解析为MAC地址。(数据的封包传输中,既需要知道对方IP,也需要知道MAC)
RARP: RARP协议则是与ARP相反的过程,它用于将MAC地址解析为IP地址。
0x8035 只接受发往本机的RARP类型的数据帧
0x0003 接收发往本机的MAC所有类型: ip,arp,rarp数据帧, 接收从本机发出去的数据帧,
当打开混杂模式打开的情况下,会接收到非发往本地的MAC数据帧
IP头:
拆包:
id标识: 接收方接收到这么多的数据包, 如何判别是一个包? 通过相同id标识来识别
flags: 拆包时, 如果该数据包后续还有拆好的包, 那么flags 为 M, 否则为D
面试: 1. 常用于拆包的是哪个包头? IP
2. IP头哪些内容用于拆包? id身份证明 flags
IP头有两个地址 , 是哪两个地址? 源IP地址 目的IP地址
UDP头:
UDP终端length可以有效防止udp的粘包问题,让udp有固定的长度
UDP会造成丢包, TCP会造成粘包和拆包
UDP通信没有发送缓存区, 它不保证数据的可靠性。因此,UDP通信是将数据尽快发送出去,不关心数据是否到达目标主机. 但是UDP有接受缓存区, 因为数据发送过快, 如果接收缓存区内数据已满, 则继续发送数据, 可能会出现丢包。
丢包出现原因: 接收缓存区满 网络拥堵, 传输错误
相比之下,TCP是一种面向连接的传输协议,它需要保证数据的可靠性和顺序性。TCP有发送缓存区和接收缓存区, 如果发送频率过快, 且内容小于发送缓存区的大小 , 可能会导致多个数据的粘包。如果发送的数据大于发送缓存区, 可能会导致拆包。
UDP不会造成粘包和拆包, TCP不会造成丢包
UDP是基于数据报文发送的,每次发送的数据包,在UDP的头部都会有固定的长度, 所以应用层能很好的将UDP的每个数据包分隔开, 不会造成粘包。
TCP是基于字节流的, 每次发送的数据报,在TCP的头部没有固定的长度限制,也就是没有边界,那么很容易在传输数据时,把多个数据包当作一个数据报去发送,成为了粘包,或者传输数据时, 要发送的数据大于发送缓存区的大小,或者要发送的数据大于最大报文长度, 就会拆包;
TCP不会丢包,因为TCP一旦丢包,将会重新发送数据包。(超时/错误重传)
TCP头
关于TCP Flags
Seq:序列号,所有非应答包的数据段, 都有seq,占4个字节,用于给数据段进行编号的。
Ack:应答号,用于应答(握手包,挥手包,数据包),告诉对方的下次Seq号从此次Ack号开始
以下三个包,都是由seq序列号的,因为都需要被ACK应答-----------
SYN:握手包,连接的时候产生的包
FIN:挥手包,断开连接产生的包
PSH:数据包,传输数据时候产生的包
ACK:应答包 - 无seq序列号,因为不需要被回复名
(抓包中出现的ACK包内的Seq不用管)
【5】关于TCP建立可靠通信的原因:
1.序列号和确认机制:
每次数据传输,都有PUSH,每次PUSH包发送之后,接收者都会回复一个ACK包,TCP建立高可靠通信的原因之一:
2.三次握手机制:
服务器必须准备好接受外来的连接。这通过调用socket、 bind和listen函数来完成,称为被动打开.
第一次握手:客户通过调用connect进行主动打开。客户端发送一个SYN(表示同步)分节,它告诉服务器客户将在连接中发送到数据的初始序列号。并进入SYN_SEND(发送)状态,等待服务器的确认。
第二次握手:服务器必须确认客户的SYN,同时自己也得发送一个SYN分节,它含有服务器将在同一连接中发送的数据的初始序列号。服务器以单个字节向客户发送SYN和对客户SYN的ACK(表示确认),此时服务器进入SYN_RECV(接收)状态。
第三次握手:客户收到服务器的SYN+ACK。向服务器发送确认分节,此分节发送完毕,客户端和服务器进入ESTABLISHED(确认)状态,完成三次握手。
3.四次挥手机制:
第一次挥手:主动关闭方发送一个FIN(挥手包)给被动方,进入FIN_WAIT状态;
第二次挥手:被动方接收到FIN(挥手)包,给主动方发送一个ACK(确认)包;并进入CLOSE_WAIT状态,主动方接收到ACK包后,进入FIN_WAIT状态,如果有数据没有发送完毕,则继续发送,一直到发送完毕;
第三次挥手:被动方发送一个FIN包,进入LAST_ACK(最后确认)状态。
第四次挥手:主动关闭方收到FIN包,回复一个FIN包。被动关闭方收到主动关闭方的ACK后关闭连接。
4.超时重传机制:
如果在指定时间内发送方没有收到确认消息(Ack),发送方会认为数据丢失,重新发送数据。
1.关于TCP建立可靠通信的原因
2.TCP与UDP的区别,特点