tcp
select poll epoll(通知机制多个回调)
1: mac帧头和ip头和udp头
ip头是核心,提供不可靠、无连接的服务,也即依赖其他层的协议进行差错控制。
IP协议往往被封装在以太网帧发送。
而所有的TCP、UDP、ICMP、IGMP数据都被封装在IP数据报中传送。
/*数据帧定义,头14个字节,尾4个字节*/
typedef struct _MAC_FRAME_HEADER
{
char m_cDstMacAddress[6]; //目的mac地址
char m_cSrcMacAddress[6]; //源mac地址
short m_cType; //上一层协议类型,如0x0800代表上一层是IP协议,0x0806为arp
}__attribute__((packed))MAC_FRAME_HEADER,*PMAC_FRAME_HEADER;
typedef struct _MAC_FRAME_TAIL
{
unsigned int m_sCheckSum; //数据帧尾校验和
}__attribute__((packed))MAC_FRAME_TAIL, *PMAC_FRAME_TAIL;
ip头
udp头
/*IP头定义,共20个字节*/
typedef struct _IP_HEADER
{
char m_cVersionAndHeaderLen; //版本信息(前4位),头长度(后4位)
char m_cTypeOfService; // 服务类型8位
short m_sTotalLenOfPacket; //数据包长度
short m_sPacketID; //数据包标识
short m_sSliceinfo; //分片使用
char m_cTTL; //存活时间
char m_cTypeOfProtocol; //协议类型
short m_sCheckSum; //校验和
unsigned int m_uiSourIp; //源ip
unsigned int m_uiDestIp; //目的ip
} __attribute__((packed))IP_HEADER, *PIP_HEADER ;
/*UDP头定义,共8个字节*/
typedef struct _UDP_HEADER
{
unsigned short m_usSourPort; // 源端口号16bit
unsigned short m_usDestPort; // 目的端口号16bit
unsigned short m_usLength; // 数据包长度16bit
unsigned short m_usCheckSum; // 校验和16bit
}__attribute__((packed))UDP_HEADER, *PUDP_HEADER;
2:TCP的头部信息
/*TCP头定义,共20个字节*/
typedef struct _TCP_HEADER
{
//端口号和IP地址,可以唯一确定一个TCP连接,在网络编程中,通常被称为一个socket接口
short m_sSourPort; // 源端口号16bit
short m_sDestPort; // 目的端口号16bit
//序号:占4字节,用来标识从TCP发送端向TCP接收端发送的数据字节流。
unsigned int m_uiSequNum; // 序列号32bit
//确认序号:占4字节,包含发送确认的一端所期望收到的下一个序号,
//因此,确认序号应该是上次已经成功收到另一端数据字节序号加1
unsigned int m_uiAcknowledgeNum; // 确认号32bit
//数据偏移:占4位,最大为1111即15个数字,一个1代表4个字节,用于指出TCP首部长度,
//若不存在选项,则这个值为20字节,数据偏移的最大值为60字节
//保留字段:占6位,值是确定的,暂时可忽略,值全为0。
/*标志位:
RG(紧急): 为1时表明紧急指针字段有效。
ACK(确认):为1时表明确认号字段有效。
PSH(推送):为1时接收方应尽快将这个报文段交给应用层。
RST(复位):为1时表明TCP连接出现故障必须重建连接。
SYN(同步):在连接建立时用来同步序号。
FIN (终止): 为1时表明发送端数据发送完毕要求释放连接
*/
short m_sHeaderLenAndFlag; // 前4位:TCP头长度;中6位:保留;后6位:标志位
/*
接收窗口:占2个字节,用于流量控制和拥塞控制,表示当前接收缓冲区的大小。
在计算机网络中,通常是用接收方的接收能力的大小来控制发送方的数据发送量,
这样可以避免快主机致使较慢主机的缓冲区溢出。
TCP连接的一端根据缓冲区大小确定自己的接收窗口值,告诉对方,使对方可以确定发送数据的字节数。
*/
short m_sWindowSize; // 窗口大小16bit
//校验和:占2个字节,范围包括首部和数据两部分。检查当前的TCP包是否有问题,有没有损坏丢失
short m_sCheckSum; // 检验和16bit
short m_surgentPointer; // 紧急数据偏移量16bit
}attribute((packed))TCP_HEADER, *PTCP_HEADER;
/*TCP头中的选项定义
kind(8bit)+Length(8bit,整个选项的长度,包含前两部分)+内容(如果有的话)
KIND:
1表示 无操作NOP,无后面的部分
2表示 maximum segment 后面的LENGTH就是maximum segment选项的长度(以byte为单位,1+1+内容部分长度)
3表示 windows scale 后面的LENGTH就是 windows scale选项的长度(以byte为单位,1+1+内容部分长度)
4表示 SACK permitted LENGTH为2,没有内容部分
5表示这是一个SACK包 LENGTH为2,没有内容部分
8表示时间戳,LENGTH为10,含8个字节的时间戳
*/
typedef struct _TCP_OPTIONS
{
char m_ckind;
char m_cLength;
char m_cContext[32];
}__attribute__((packed))TCP_OPTIONS, *PTCP_OPTIONS;
3:TCP的状态,三次握手,四次挥手,
11种状态
1.CLOSED状态:初始状态,表示TCP连接是“关闭的”或者“未打开的”。
2.LISTEN状态:表示服务端的某个端口正处于监听状态,正在等待客户端连接的到来。
3.SYN_SENT状态:当客户端发送SYN请求建立连接之后,客户端处于SYN_SENT状态,等待服务器发送SYN+ACK。
4.SYN_RCVD状态:当服务器收到来自客户端的连接请求SYN之后,服务器处于SYN_RCVD状态,在接收到SYN请求之后会向客户端回复一个SYN+ACK的确认报文。
5.ESTABLISED状态:当客户端回复服务器一个ACK和服务器收到该ACK(TCP最后一次握手)之后,服务器和客户端都处于该状态,表示TCP连接已经成功建立。
6.FIN_WAIT_1状态:当数据传输期间当客户端想断开连接,向服务器发送了一个FIN之后,客户端处于该状态。
7.FIN_WAIT_2状态:当客户端收到服务器发送的连接断开确认ACK之后,客户端处于该状态。
8.CLOSE_WAIT状态:当服务器发送连接断开确认ACK之后但是还没有发送自己的FIN之前的这段时间,服务器处于该状态。
9.TIME_WAIT状态:当客户端收到了服务器发送的FIN并且发送了自己的ACK之后,客户端处于该状态。
10.LAST_ACK状态:表示被动关闭的一方(比如服务器)在发送FIN之后,等待对方的ACK报文时,就处于该状态。
11.CLOSING状态:连接断开期间,一般是客户端发送一个FIN,然后服务器回复一个ACK,然后服务器发送完数据后再回复一个FIN,当客户端和服务器同时接受到FIN时,客户端和服务器处于CLOSING状态,也就是此时双方都正在关闭同一个连接。
4:TCP相关问题
1:为何最后一次ACK之后需要等待2MSL的时间?
网络是不可靠的,TCP是可靠协议,必须保证最后一次报文送达之后才能断开链接,否则会再次收到S端的FIN报文信息。而等待2MSL时间就是为了保证最后最后一次报文丢失时还能重新发送。
2:为何是2MSL(2~四分钟)的时间
MSL(报文段最大生存时间)
2MSL是报文一个往返的最长时间,假设小于这个时间会发生,ACK丢了,但是还没接收到对方重传的FIN我方就重新发送了ACK。1. 为了防止最后一个ACK丢失。如果ACK丢失,接收方会重新发出FIN,这时发送方需要重发ACK。 可以避免接收方反复超时重传。
\2. 防止lost duplicatie + incarnation connection的出现。
lost duplicate指的是由于网络拥塞而延迟到达的、已经失效的重复包,
incarnation connection指的是新的连接和原来的socket pair一模一样。
当这两种情况同时出现时,很可能造成数据混乱。
等待2MSL,可以让lost duplicate的包在网络中消失。
3:如果已经建立了连接,但是客户端突然出现故障了怎么办?
TCP自己做了保证,TCP默认有个定时器,每次收到客户端的请求后会把定时器设置好,通常设置两小时,超过两小时还没收到数据,服务端会发送一个探测报文,以后每隔75秒钟发送一次。若一连发送10个探测报文仍然没反应,服务器就认为客户端出了故障,接着就关闭连接。
4: 3次握手,如果最后一个ACK丢失,会怎样。
Answer:接收端会等待ACK超时,然后重新发送之前的SYN+ACK,并且超时会翻倍增加避免网络拥塞。这时候连接还没有建立。如果发送方发送数据报文,会收到接收方返回的RST包。
5:三次握手哪个阶段容易出现攻击?哪个阶段会出现异常?
SYN溢出攻击。
出现在第二阶段,如果客户端伪造大量的SYN同步报文,服务端就会依次消耗掉很多资源来保存客户端信息,并进行确认,实际确认是会失败的,但失败需要一定的时间,因为服务器会连续多次进行第二次握手确认后才认定失败。短时间内会有大量的SYN同步报文涌向服务端,服务器资源可能被耗尽,就可能导致正常的客户端得不到响应而失败。
第二阶段可能出现异常,如果服务器相应的端口未打开,会回复RST复位报文,握手失败。此外,listen创建的监听队列达到上限,也可能失败。
防范syn攻击:修改tcp协议实现。主要方法有SynAttackProtect保护机制、SYN cookies技术、增加最大半连接和缩短超时时间等.
但是不能完全防范syn攻击。
6:为什么是四次挥手?三次可以么?可以的话,在什么情况可以三次能完成?
因为TCP连接是全双工的也就是说接收到FIN只是说没有数据再发过来但是还是可以发送数据的,也就是接受到一个FIN只是关闭了一个方向的数据传输,另一个方向还可以继续发送数据。
三次是可以的,当本端关闭了连接,恰好也同时收到了对方的FIN报文,此时可以把自己的FIN和给对端的确认ACK合在一起发送,就变成了三次。
7:TIME_WAIT和CLOSE_WAIT状态有什么区别?
CLOSE_WAIT是被动关闭的一端在接收到对端关闭请求(FIN报文段)并且将ACK发送出去后所处的状态,这种状态表示:收到了对端关闭请求,但是本端还没有完成工作,还未关闭。
TIME_WAIT状态是主动关闭的一端在本端已经关闭的前期下,收到对端的关闭请求并且将ACK发送出去后所处的状态。
这种状态表示:双方都已经完成工作,只是为了确保迟来的数据报能被识别并丢弃,可靠的终止TCP连接。
8:TCP和UDP用一个端口发送信息是否冲突?
不冲突。TCP和UDP可以绑定同一个端口进行通信。因为数据接收时根据五元组(传输协议、源IP、目的IP、源端口、目的端口)判断接受者的
9:TCP协议和UDP协议的区别是什么
-
TCP协议是有连接的,有连接的意思是开始传输实际数据之前TCP的客户端和服务器端必须通过三次握手建立连接,会话结束之后也要结束连接。而UDP是无连接的
-
TCP协议保证数据按序发送,按序到达,提供超时重传来保证可靠性,但是UDP不保证按序到达,甚至不保证到达,只是努力交付,即便是按序发送的序列,也不保证按序送到。
-
TCP协议所需资源多,TCP首部需20个字节(不算可选项),UDP首部字段只需8个字节。
-
TCP有流量控制和拥塞控制,UDP没有,网络拥堵不会影响发送端的发送速率
-
TCP是一对一的连接,而UDP则可以支持一对一,多对多,一对多的通信。
-
TCP面向的是字节流的服务,UDP面向的是报文的服务。
10:以下应用一般或必须用udp实现?
-
多播的信息一定要用udp实现,因为tcp只支持一对一通信。
-
如果一个应用场景中大多是简短的信息,适合用udp实现,因为udp是基于报文段的,它直接对上层应用的数据封装成报文段,然后丢在网络中,如果信息量太大,会在链路层中被分片,影响传输效率。
-
如果一个应用场景重性能甚于重完整性和安全性,那么适合于udp,比如多媒体应用,缺一两帧不影响用户体验,但是需要流媒体到达的速度快,因此比较适合用udp
-
如果要求快速响应,那么udp听起来比较合适
-
如果又要利用udp的快速响应优点,又想可靠传输,那么只能考上层应用自己制定规则了。
-
常见的使用udp的例子:ICQ,QQ的聊天模块。
11: tcp状态time_wait
客户端:
当通信时使用短连接,并由客户端主动关闭连接时,主动关闭连接的客户端会产生TIME_WAIT状态的连接,一个TIME_WAIT状态的连接就占用了一个本地端口。这样在TIME_WAIT状态结束之前,本地最多就能承受6万个TIME_WAIT状态的连接,就无端口可用了。
客户端与服务端进行短连接的TCP通信,如果在同一台机器上进行压力测试模拟上万的客户请求,并且循环与服务端进行短连接通信,那么这台机器将产生4000个左右的TIME_WAIT socket,后续的短连接就会产生address already in use : connect的异常。
RST:重置连接、复位连接,用来关闭异常的连接
关闭的时候使用RST的方式,不进入 TIME_WAIT状态,是否可行?
当通信时使用短连接,并由服务端主动关闭连接时,主动关闭连接的服务端会产生TIME_WAIT状态的连接。
由于都连接到服务端80端口,服务端的TIME_WAIT状态的连接会有很多个。
假如server一秒钟处理1000个请求,那么就会积压240秒*1000=24万个TIME_WAIT的记录,服务有能力维护这24万个记录。
服务端:
大多数服务器端一般执行被动关闭,服务器不会进入TIME_WAIT状态。(nginx代理也会出现,因为短连接)
服务端为了解决这个TIME_WAIT问题,可选择的方式有三种:
Ø 保证由客户端主动发起关闭(即做为B端)
Ø 关闭的时候使用RST的方式
Ø 对处于TIME_WAIT状态的TCP允许重用
修改配置文件/etc/sysctl.conf,使服务器能够快速回收和重用那些TIME_WAIT的资源。
12:处于大量TIME_WAIT状态,你无法避免,那是TCP协议的一部分,端口无法使用,会报错:"bind: address in use",
解决:调用bind()之前设置SO_REUSEADDR套接字选项,通知内核,如果端口忙,但TCP状态位于 TIME_WAIT ,可以重用 端口。
修改配置文件/etc/sysctl.conf,使服务器能够快速回收和重用那些TIME_WAIT的资源。
原理:IME_WAIT状态可以通过优化服务器参数得到解决,因为发生TIME_WAIT的情况是服务器自己可控的,
要么就是对方连接的异常,要么就是自己没有迅速回收资源,总之不是由于自己程序错误导致的。
13:服务器处于大量CLOSE_WAIT状态 ==》netstat -an
从上面的图可以看出来,如果一直保持在CLOSE_WAIT状态,那么只有一种情况,就是在对方关闭连接之后服务器程序自己没有进一步发出ack信号。
换句话说,就是在对方连接关闭之后,程序里没有检测到,或者程序压根就忘记了这个时候需要关闭连接,于是这个资源就一直被程序占着。个人觉得这种情况,通过服务器内核参数也没办法解决,服务器对于程序抢占的资源没有主动回收的权利,除非终止程序运行。
14:connect(), listen()和accept()函数
connect():为客户端主动连接服务器,建立连接是通过三次握手,而这个连接的过程是由内核完成,不是这个函数完成的,这个函数的作用仅仅是通知 Linux 内核,让 Linux 内核自动完成 TCP 三次握手连接.==》 阻塞等待
listen() :int listen(int sock, int backlog); //Linux
服务端被动连接的,主要作用就是将套接字( sockfd )变成被动连接的监听套接字(被动等待客户端的连接),至于参数 backlog 的作用是设置内核中连接队列的长度。
listen()函数不会阻塞,它主要做的事情为,将该套接字和套接字对应的连接队列长度告诉 Linux 内核,然后,listen()函数就结束。
listen() 函数的第二个参数( backlog)的作用:告诉内核连接队列的长度。
为了更好的理解 backlog 参数,我们必须认识到内核为任何一个给定的监听套接口维护一个队列,该队列由两部分构成,分别是完成连接接队列、未完成连接队列:
1、未完成连接队列(incomplete connection queue),当服务器每收到客户端的一个SYN分节,就会将该客户端放入未完成连接队列,而服务器套接口处于 SYN_RCVD 状态。
2、已完成连接队列(completed connection queue),当客户端和服务器彻底完成三次握手过程,客户端将从未完成连接队列升级成已完成连接队列,并从未完成连接队列中清空该客户端,这些套接口处于 ESTABLISHED 状态。
backlog 参数历史上被定义为上面两个队列的大小之和,大多数实现默认值为 5,(可以防止syn泛洪攻击)当服务器把这个完成连接队列的某个连接取走后,这个队列的位置又空出一个,这样来回实现动态平衡。
accept():从连接队列头部取出一个已经完成的连接,如果这个队列没有已经完成的连接,accept()函数就会阻塞,直到取出队列中已完成的用户连接为止。 (队列满了会拒绝连接?延迟连接?)
5:tcp关闭状态转移图
状态迁移:
客户端:CLOSED->SYN_SENT->ESTABLISHED->FIN_WAIT_1->FIN_WAIT_2->TIME_WAIT->CLOSED
服务端:CLOSED->LISTEN->SYN收到->ESTABLISHED->CLOSE_WAIT->LAST_ACK->CLOSED
其他:LISTEN->SYN_SENT,对于这个解释就很简单了,服务器有时候也要打开连接的嘛。
SYN_SENT->SYN收到,服务器和客户端在SYN_SENT状态下如果收到SYN数据报,则都需要发送SYN的ACK数据报并把自己的状态调整到SYN收到状态,准备进入ESTABLISHED
SYN_SENT->CLOSED,在发送超时的情况下,会返回到CLOSED状态。
SYN_收到->LISTEN,如果受到RST包,会返回到LISTEN状态。
SYN_收到->FIN_WAIT_1,这个迁移是说,可以不用到ESTABLISHED状态,而可以直接跳转到FIN_WAIT_1状态并等待关闭
一端异常关闭:用心跳包或者服务器keepalive保活。
6:tcp相关问题
1:TCP快速重传
TCP等待对端回应,syn=n,因为回应顺序的差异,可能不是丢失,回复3次可以确定n丢失,直接重发
2:TCP如果保证可靠性
连接管理:三次握手和四次挥手
校验和:发送的数据包的二进制相加然后取反,确保
确认应答+序列号:TCP给发送的每一个包进行编号,接收方对数据包进行排序,把有序数据传送给应用层
超时重传(定时器): 不能及时收到一个确认,将重发这个报文段(时间是动态计算的)
超时以500ms(0.5秒)为一个单位进行控制,每次判定超时重发的超时时间都是500ms的整数倍,到一定的重传次数,TCP就认为网络或者对端出现异常,强制关闭连接。
流量控制(滑动窗口):接收端只允许发送端发送接收端缓冲区能接纳的数据
接收方有即时窗口(滑动窗口),随ACK报文发送
发送方有拥塞窗口,发送数据前比对接收方发过来的即使窗口,取小
在TCP协议的报头信息当中,有一个16位字段的窗口大小。窗口大小的内容实际上是接收端接收数据缓冲区的剩余大小。这个数字越大,证明接收端接收缓冲区的剩余空间越大,网络的吞吐量越大。
接收端会在确认应答发送ACK报文时,将自己的即时窗口大小填入,并跟随ACK报文一起发送过去。而发送方根据ACK报文里的窗口大小的值的改变进而改变自己的发送速度。
如果接收到窗口大小的值为0,那么发送方将停止发送数据。并定期的向接收端发送窗口探测数据段,让接收端把窗口大小告诉发送端。
16位的窗口大小最大能表示65535个字节(64K),但是TCP的窗口大小最大并不是64K(窗口扩大因子)
慢启动、拥塞避免、拥塞发送、快速恢复
拥塞控制:1)慢启动,2)拥塞避免,3)拥塞发生,4)快速恢复。
发送刚开始定义拥塞窗口为 1,每次收到ACK应答,拥塞窗口加 1。在发送数据之前,首先将拥塞窗口与接收端反馈的窗口大小比对,取较小的值作为实际发送的窗口。
拥塞窗口的增长是指数级别的。
设置一个拥塞窗口的阈值,当拥塞窗口大小超过阈值时,不能再按照指数来增长,而是线性的增长。
在慢启动开始的时候,慢启动的阈值等于窗口的最大值,一旦造成网络拥塞,发生超时重传时,慢启动的阈值会为原来的一半(这里的原来指的是发生网络拥塞时拥塞窗口的大小),同时拥塞窗口重置为 1。
7:TCP定时器:
-
重传定时器 Retransmission Timer
-
坚持定时器 Persistent Timer
-
保活定时器 Keeplive Timer
-
2MSL定时器 Time_Wait Timer
8:TCP代码:
/*******************************************
定义tcp协议栈,实现tcp通信
第一:定义特定的用户数据,进行解析。 socket自带的tcp接口
第二:定义协议栈,UDP,TCP从网卡接收到数据,直接解析。
第三:基于socket,对其进行封装使用。
********************************************/
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h> //inet_pton
struct protoDada{
int type;
int length;
char body[0];
};
int ClientMain()
{
//创建socket 连接对端 发送数据 接收数据
int cli_sock_fd = socket(AF_INET, SOCK_STREAM, 0);
if(cli_sock_fd == -1)
{
printf("ERROR create cli socket error. \n");
close(socket_fd);
return -1;
}
//去连接
struct sockaddr_in addr;
memset(&addr, 0, sizeof(addr));
const char* ip = "0.0.0.0";
addr.sin_family = AF_INET;
addr.sin_port = htons((short)7000);
inet_pton(AF_INET, ip, &addr.sin_addr);
while(true)
{
if(connect(cli_sock_fd, (struct sockaddr*)&addr, sizeof(addr)) == -1)
{
printf("ERROR connect server socket error. \n");
return -1;
}
char sendData[] = "client Data test.";
send(cli_sock_fd, sendData, strlen(sendData),0);
char recvData[255];
int ret = recv(cli_sock_fd, recvData, 255, 0);
if(ret>0)
{
recvData[ret] = '\0';
printf("recvData: %s\n", recvData);
}
usleep(1000);
}
close(cli_sock_fd);
return 0;
}
int ServerMain()
{
//定义socket 绑定端口, 开始监听, accept等待连接, 负责接收, 返回结果
int socket_fd = socket(AF_INET, SOCK_STREAM , 0);
if(socket_fd == -1)
{
printf("ERROR: create server socket error. \n");
return -1;
}
struct sockaddr_in addr;
memset(&addr, 0, sizeof(addr));
addr.sin_family = AF_INET; //ipv4
addr.sin_port = htons((short)7000);
inet_pton(AF_INET, ip, &addr.sin_addr);//ip转换 反转用inet_ntop
//如果要设置端口重用,就用setsockopt,SO_REUSEADDR==》远端地址其实不同 同一个socket绑定多个地址
int opt = 1;
if(setsockopt(socket_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)) == -1)
{
printf("ERROR: setsocketopt error\n");
}
if(bind(socket_fd, (struct sockaddr*)&addr, sizeof(addr)) == -1)
{
printf("ERROR: bind address error \n")
close(socket_fd);
return -1;
}
if(listen(socket_fd, 1024) == -1)
{
printf("ERROR: listen server socket error \n");
close(socket_fd);
return -1;
}
while(true)
{
int client_fd;
int len = sizeof(addr);
client_fd = accept(socket_fd, (struct sockaddr *)&addr, &len);
if(client_fd == -1)
{
printf("ERROR:accept error -1 \n");
break;
}
char recvData[255] = {0};
int ret = recv(client_fd, recvData, 255, 0);
if(ret>0)
{
recv[ret] = 0x00;
printf("recv: %s \n", recvData);
}
char* sendData = "hello client \0";//char sendData[] = "hello client";
send(client_fd, sendData, strlen(sendData),0);
}
close(socket_fd);
return 0;
}