知识点1【TCP编程概述】
1、TCP的概述
客户端:主动连接服务器,和服务器进行通信
服务器:被动被客户端连接,启动新的线程或进程,服务器客户端(并发服务器)
这里重复TCP和UDP特点
TCP(传输控制协议):是一种靠谱的传输层协议,
买电话 买电话卡 开声音 按下接听
知识点2【TCP客户端编程】
1、创建TCP套接字
SOCK_STREAM
socket函数创建的TCP套接字,没有端口,且默认是主动连接特性
2、connect函数连接服务器
#include <sys/types.h>
#include <sys/socket.h>
int connect(int sockfd, const struct sockaddr *addr,
socklen_t addrlen);
功能介绍
客户端主动发出与TCP服务器的连接(三次握手)
参数
sockfd:客户端套接字
addr:只想服务器的地址结构体
addrlen:地址结构体的长度
返回值
成功:0
失败:返回-1
注意
TCP客户端通信之前,必须实现 建立和服务器之间的连接
connect连接成功一个服务器,不能再次连接其他服务器
inet_addr()(补充函数,平替pton)函数仅支持IPv4
如果socket没有固定端口,在调用connect时系统自动分配随机端口为源端口
connect实际上是带阻塞的,需要三次握手
3、send发送消息
#include <sys/types.h>
#include <sys/socket.h>
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
功能介绍
通过已连接套接字发送数据
参数
sockfd:已连接套接字(下面accept函数中会介绍)
buf:带发送数据的缓冲区指针
len:要发送的字节数
flags:控制标志
MSG_OOB
:发送带外数据(紧急数据)。MSG_DONTWAIT
:非阻塞发送(立即返回)。MSG_NOSIGNAL
:禁止发送SIGPIPE
信号。
返回值
成功:返回实际发送的字节数
失败:返回-1
注意
TCP不能发出0长度报文,但是UDP可以。
4、recv接收数据(默认阻塞)
#include <sys/types.h>
#include <sys/socket.h>
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
功能介绍
从已连接套接字接收数据
参数
sockfd:已连接套接字
buf:接收数据的缓冲区指针
len:缓冲区最大容量
falgs:控制标记
返回值
成功:返回实际接收到的字节数
失败:返回-1
注意
recv如果收到0长度报文,表明对方已经断开连接,因此我们使用send的时候,不能发送0长度报文
只要一方关闭,另外一方就会收到0长度报文
知识点3【TCP服务器编程】
我们先把服务器的过程形象化:
1、socket() 买手机
2、bind() 买电话卡
3、listen() 打开声音
4、accept() 接听
1、作为服务器的条件
1、需要为服务器绑定一个固定的端口、IP(连接作用)
2、让操作系统知道这是一个服务器,而不是客户端
(使用listen函数让服务器具备监听功能,使套接字由主动变被动)
3、等待客户端的连接到来,使用accept提取到来的客户端
2、listen监听函数
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int listen(int sockfd, int backlog);
功能介绍
将套接字设为被动监听模式,等待客户端连接
参数
sockfd:套接字
backlog:连接队列的最大长度
返回值
成功:0
失败:-1
函数功能详解(底层)
1、将sockfd由主动变被动,并且对sockfd进行监听客户端连接的到来
2、backlog是连接队列的大小,表示客户端的最大个数
连接队列大小分析:
1、在listen后,在TCP服务器中,套接字改称为监听套接字,所有客户端想要连接,就需要向这个客户端发送连接请求
2、客户端发出连接请求(connect),TCP的server就会创建一个连接队列
3、连接队列又会分为两部分,但是总大小是上面对应的个数
(1)完成连接——三次握手之后
(2)未完成连接——三次握手完成之前
图形解析
这里说一个早期的一个攻击手段:SYN洪流攻击
就是一直发送低于三次握手信号(半成品),这样的信号全部在连接队列的未完成连接中存储,服务器也无法处理,当达到连接队列的最大值后,正常连接反而进不去连接队列。
这里
我们写监听函数,并阻塞,查看其网络状态
netstate -anp | grep a.out
可以看到此时处于监听状态(想系统说明此时是一个服务器)
3、accept提取客户端的连接(阻塞)
#include <sys/socket.h>
int accept(int socket, struct sockaddr *restrict address,
socklen_t *restrict address_len);
函数功能
accept只能从 连接队列 中 处于 完成连接部分 中提取连接
将提取到的该客户端的信息存到addr中
将提取到的该客户端从连接队列中删除
参数
sockfd:监听套接字
addr:存放的是 客户端 的地址信息
addrlen:地址结构体的长度的地址
返回值
成功:返回一个已连接套接字,这个套接字才代表服务器和该客户端的连接端点(真正和客户端的连接)
解释:如果服务器想要和该客户端通信,就需要向这个 已连接套接字 中读写
失败:返回-1
注意
调用一次,只能提取一个客户端对应的连接,如果连接队列没有客户端连接,将阻塞
因为是从 连接队列 中提取的原因,遵循先进先出的原则
验证一下:通过两个客户端,都连接我们写的服务器,会发现只有一个能发送
TCP服务器代码演示
#include <stdio.h>
#include <string.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
int main(int argc, char const *argv[])
{
//创建监听套接字
int fd_sock_lis = socket(AF_INET,SOCK_STREAM,0);
if(fd_sock_lis < 0)
{
perror("socket_lis");
_exit(-1);
}
//绑定固定端口 这里我们设置为8000
struct sockaddr_in addr_bind;
bzero(&addr_bind,sizeof(addr_bind));
addr_bind.sin_family = AF_INET;
addr_bind.sin_port = htons(8000);
addr_bind.sin_addr.s_addr = htonl(INADDR_ANY);
int ret_bind = bind(fd_sock_lis,(struct sockaddr *)&addr_bind,sizeof(addr_bind));
if(ret_bind < 0)
{
perror("bind");
_exit(-1);
}
//监听 参数:监听套接字,连接队列的个数
int ret_listen = listen(fd_sock_lis,10);
if(ret_listen == -1)
{
perror("listen");
_exit(-1);
}
//提取客户端连接,已连接套接字的创建,已连接套接字理解为通信端口的一端
//服务器通过操作这个已连接套接字与客户端进行联系
struct sockaddr_in addr_accept;
bzero(&addr_accept,sizeof(addr_accept));
int len_accept = sizeof(addr_accept);
int fd_sock_connect = accept(fd_sock_lis,(struct sockaddr *)&addr_accept,&len_accept);
if(fd_sock_connect < 0)
{
perror("accept");
_exit(-1);
}
//接收数据,从已连接套接字中接收
char arr_recv[256] = "";
recv(fd_sock_connect,arr_recv,sizeof(arr_recv),0);
//处理 客户端端口 的信息
int port_client = ntohs(addr_accept.sin_port);
char ip_client[16] = "";
inet_ntop(AF_INET,&addr_accept.sin_addr.s_addr,ip_client,sizeof(ip_client));
//遍历接收到的信息
printf("从IP:%s的%d端口,获取的数据是%s\\n",ip_client,port_client,arr_recv);
//发送应答
send(fd_sock_connect,"ok",sizeof("ok"),0);
//关闭 所有套接字
close(fd_sock_connect);
close(fd_sock_lis);
return 0;
}
代码运行结果
知识点4【close关闭套接字】
当客户端用完后会调用close套接字,服务器也要关闭套接字(监听套接字,已连接套接字)
1、作为客户端
close(套接字),断开当前的连接,导致服务器收到0长度报文
2、作为服务器
close(监听套接字),该服务器不能监听新的连接的到来,但是不影响已连接客户端的通信
close(已连接套接字),只是断开当前客户端的连接,不会影响监听套接字(服务器可以继续监听新的连接的到来)
这里我们形象化一下
形象化理解
媒婆 监听套接字
小帅 客户端套接字
小美 已连接套接字
小帅找媒婆介绍对象,媒婆有个名单,把小美介绍给了小帅,小美和小帅分还是合,最后不会影响媒婆招揽其他生意
如果媒婆不想干媒婆这一行了,去转学嵌入式了,也不会影响小帅和小美的关系
3、下面内容前提
握手,挥手都涉及到底层,我们需要用抓包的操作来了解这一过程,我这里用的抓包工具是 wireshark
这是 我使用的抓包工具获取的上面的发送过程
由于我的 图不太好识别
我用我老师当时的抓包图片(很标准的三次握手,数据通信,四次挥手抓包过程)
我们分为三个流程 1、三次握手,2、数据通信,3、四次挥手 这里大家先看一下总的流程图,流程图是当时老师画的,我感觉很牛,这里直接使用了
知识点4【三次握手】(重要 背!!)
当客户端调用connect连接服务器,底层会完成三次握手信号,此时客户端阻塞,当三次握手信号完成,connect才会解除阻塞往下执行
三次握手的发起者 是客户端
1、TCP的头部
这里先介绍一下 TCP的头部
1、SYN:置1,表示报文是 连接请求报文
2、FIN:置1,表示报文是 关闭请求报文
3、ACK:置1,表示报文是 回应(应答)报文
4、URG:置1,表明紧急指针字段有效,告诉操作系统此报文内有紧急数据,请尽快传送
5、PUSH:置1:推送报文
6、RST:置1:复位连接
注意
序列号:seq,当前报文的编号。
确认序号:当前报文希望接下来对方发送的报文编号
ack(确认序号)数据分析
1、当无数据时,ack = 对方原编号 + 1
2、当 有数据时,ack = 对房源编号 + 接收到数据长度
2、三次握手
分析过程
1、当客户端调用connect后,底层发送SYN连接请求
此时seq = 0,ack = 0
2、服务器收到客户端发出的SYN请求,服务器给客户端回应SYN和ACK应答
此时seq = 1,ack = 1(无数据)
3、客户端接收到服务器发送的SYN请求后,向服务器发送ACK应答
此时seq = 1,ack = 1(原本服务器seq = 0,无数据+1后为1)
形象化理解
小帅给小美表白
小帅:我喜欢你(SYN)
小美:我知道(ACK),我也喜欢你(SYN)
小帅:其实我也知道(ACK)
3、分析通信过程
这里客户端发送的时”hello ack“,服务器应答我们设置的时”ok”
现在我们分析一下(每个红线是一个步骤)
分析过程
1、客户端和服务器经过三次握手后已经 完成连接(连接列表),客户端向服务器发送”hello ack“
这里有一个技巧:连续的线都是一个方向时,线对应的seq和ack是一样的
此时seq = 1,ack = 1,len = 9
2、服务器收到数据后,及其长度,发出ACK应答
此时seq = 1(与上面的ack相对应),ack = 10( 1+len(9))
3、服务器发送数据“ok”后,客户端接收
此时seq = 1,ack = 10(线 连续且方向一样),len = 2
4、客户端接收数据,发出ACK应答
此时seq = 10,ack = 3
确认序号的作用
1、当发送端数据时500位的时候,接收时仅收到了256位
此时len = 500,但实际发送的时候 接受到的长度的时256
这时,ack的值位 对方原序号码 + 256
接收端收到后,用ack - 原序号吗算出 接收数据长度,发现与发送数据长度不同,底层会处理后,继续发送没有接收的数据。
2、检验是否发生错误传输,这里不多介绍
4、四次挥手
这里补充一个概念:FIN标志位置1的前提是调用close函数
服务器和客户端都可以先退出,一般都是客户端先退出
分析过程
1、当客户端调用close(套接字)函数,触发底层发出FIN断开连接的请求。
2、服务器收到FIN关闭请求,做出ACK应答(服务器进入CLOSE_WAIT状态)。
CLOSE_WAIT状态是指对方套接字已经关闭,等待 服务器 已连接套接字关闭,即调用close(对应已连接套接字)
3、服务器应用层调用close函数后,触发底层发送FIN。
4、客户端收到FIN请求,回应ACK。
以上内容依次是四次挥手
下面我想补充说明一些知识点
补充(重要)
1、细心的同学已经发现了,在第三次回收后,客户端的状态时TIME_WAIT,它在等上面?
第四次回收 涉及到服务器到CLOSE的关闭状态的转换,因此需要保证其正确执行
TIME_WAIT等待是有时间时间限制的,在规定时间内,发现服务器没有重发FIN(即二次挥手),就意味着,ACK(第四次挥手)已经成功被服务器收到,客户端也会变为CLOSE状态。
如果在规定时间内,客户端又收到了(第三次挥手),表明出现了问题,此时客户端会再次四次挥手,然后重复等待。
2、补充问题(技术面常问问题 )
(1)为什么要四次挥手,而不能向三次握手一样,在第二次握手中一起将ACK和SYN一起发送给客户端?
因为第三次挥手的触发条件是,等待服务器调用close(已连接套接字),有一个等待的过程,因此不能和ACK一起发送。
(2)客户端都已经调用了close为什么还能执行 四次挥手的操作?
因为close只是关闭客户端中的套接字,此时是半关闭状态,仅关闭这个套接字应用层的数据的收发,而将FIN,ACK这些标志位置1的操作,是在底层实现的,并不是close负责的,因此可以执行四次挥手的操作。
5、状态转换
在图中大家可以看到有很多状态,下面介绍一下这些状态都是什么
下面是状态转换流程图
介绍
红色是服务器的状态转换图,蓝色是客户端的状态转换图
结束
代码重在练习!
代码重在练习!
代码重在练习!
今天的分享就到此结束了,希望对你有所帮助,如果你喜欢我的分享,请点赞收藏夹关注,谢谢大家!!!