目录
三次握手和四次挥手
三次握手
四次挥手
setsockopt
Log.hpp
Sock.hpp
main.cc
TCP通信的时候,客户端发送信息既不能太快也不能太慢,如何保证发送方发送数据既不快又不慢呢?
服务器把自己的同步接收能力告诉客户端,而服务器的接收能力又由什么决定?接收缓冲区中剩余空间的大小。
根据对方接收能力,控制数据发送速度,这种策略叫流量控制。16位窗口大小就是接收缓冲区剩余空间大小。16位窗口大小中填的是自己的接收缓冲区大小。
这是TCP报头当中的6个标记位
6个标记位:是按照1个比特位表示某种含义的,为什么需要多个标记位?
我们可能会给对方发送各种类型的TCP报文。服务端会收到大量的不同的报文,这些标记位的本质就是来标记报文类型的。利用标记位区分报文类型。
各个标记位都是什么含义?
SYN:该报文是一个连接请求报文。
FIN:该报文是一个断开连接请求的报文。
ACK:确认应答标志位,凡是该报文具有应答特征,该标志位都会被设置为1,大部分网络报文ACK都是被设置为1的。第一个连接请求报文不会被置为1.
1.如何理解连接?
因为有大量的client将来可能连接server,所以server端一定会存在大量的连接,OS管理这些连接的方式:先描述,再组织。
所谓的连接:本质其实就是内核的一种数据结构类型,建立连接成功的时候,就是在内核中创建对应的连接对象。再对多个连接对象进行某种数据结构的组织,对连接的管理也就成了对某种数据结构的增删查改。
维护连接是有成本的:内存+CPU资源。
2.如何理解三次握手?
握手的时候发的全是报文,甚至可能携带数据。并不仅仅是标志位,只不过是标志位被置为了1.
客户端发送SYN的时候,客户端的状态是SYN SENT,服务端收到一个请求而且将自己的SYN+ACK发出了,服务端的状态就是SYN RCVD,SYN SENT和SYN RCVD都是宏,当数据发出后,客户端和服务端状态其实就是1.
客户端一旦收到SYN+ACK并且将ACK发出,客户端就认为连接建立好了。状态变为ESTABUSHED
服务端收到ACK,服务端对应的三次握手就完成了,连接就建立好了,状态也变为ESTABUSHED。
三次握手期间有一个隐形的成本就是时间,未来收的时候一定晚于发送的时候。
三次握手对客户端和服务端都要起效。必须保证双方各自通信了三次
三次握手不一定要保证成功。只能保证较大概率握手成功,如客户端发送,服务器关机了。而且三次握手时,前俩次都是有应答的,最后一次客户端发出去之后没有应答,所以对于最后一次数据是否发送成功,客户端无法知晓。
如果最后一次报文丢失了,客户端只要把报文经过ACK发出,自己的状态就变为了ESTABUSHED,即客户端认为连接建立好了,但实际上,服务端可能会没收到这条消息。
1.为什么要三次握手?而不是一次,俩次,四次?
如果是一次握手,客户端可能会一次性给服务器发送大量的SYN,由于维护连接是由成本的。有可能会一次性把服务器整崩溃,一次发送大量SYN,叫做SYN洪水。
如果是俩次握手,客户端给服务端发消息,服务端给客户端返回SYN+ACK,俩次握手,服务器无法保证自己发送的SYN+ACK被对方收到,也无法感知,而服务器一旦发送处去SYN+ACK就认为自己的连接成功了,如果服务器一开始收到了大量的SYN,服务器再发送SYN+ACK,这就会导致服务器挂了大量的连接。又跟一次握手一样。
三次握手,当客户端发送最后一次ACK的时候,只要客户端把ACK发出了,就认为连接建立好了,服务器必须得收到ACK,保证三次握手完成才建立连接。如果一开始发送大量得SYN请求时,一次和俩次握手(俩次握手会出现服务器发送SYN+ACK时,客户端直接将SYN+ACK丢弃)都是服务器挂满了大量连接,而客户端可能会出现甚至没连接得情况。三次握手,服务器上有连接时,客户端必须建立好连接,服务器的连接数和客户端的连接数是等价的。三次握手以最小的成本在建立连接时,不会过度消耗服务器资源。当服务器上有连接时,客户端必须维护好连接。如果客户端攻击服务器,服务器上的连接数和客户端上的连接数一定是等价的,即俩者的资源损失是一样的。
三次握手并不能很好的保证安全问题。
通过三次握手,服务端可以嫁接同等的成本给client,验证全双工。
如果是四次握手,最后一次握手一定是服务端给客户端发的,当客户端直接丢弃最后一次ACK,此时所有的连接又都由服务端承担。
奇数次握手最后的成本都在客户端, 偶数次握手连接建立时成本都在服务端。
在握手时,尽量选择奇数次握手,的握手的目的在于把成本加在客户端,验证全双工,没必要执行五次,其次握手。
当三次握手,客户端最后一次发送ACK后立马发送数据,由于丢包服务端没收到ACK,此时客户端认为连接建立成功,一旦发送消息,由于三次握手没成功,服务端看到数据后,意识到了连接建立异常,服务器就立马给客户端回复一个报文,这个报文就是TCP报头,并且将标记位RST置为1.客户端此时关闭连接,重新开始三次握手。
RST:reset,连接重置。
RST: 对方要求重新建立连接; 我们把携带RST标识的称为复位报文段。
连接建立好了,如果对方发送消息,而我们的窗口大小是0,此时对方就不能发数据了,对方只能等,等到我们缓冲区有空间了,再继续发送消息。但如果我们缓冲区一直满,对方也不能一直等,发送方此时会设定PSH标志位。
PSH:督促对方尽快向上交付数据。
PSH: 提示接收端应用程序立刻从TCP缓冲区把数据读走。
因为TCP是具有按序到达机制的(优点),我们发送的时候,被对方上层读取到,必须得有先后顺序,如果想插队呢?
URG:紧急标志位,要配合报头中的16位紧急指针(特定的数据在有效载荷位置的偏移量),该指针所指的地方只有一个字节被称为紧急数据。
URG: 紧急指针是否有效。
如何理解四次挥手?
FIN是断开连接的标志位。当某一方要断开连接时,让FIN置1,把报文发给对方。而且这一方向另一方再也不会发送有效数据了。
ACK和FIN也可以被同时设置,服务器可以将ACK和FIN压缩在一起,即四次挥手可能会变成三次挥手。
断开连接就一定能成功吗?
也不一定,客户端首次发出断开连接的请求时,客户端会进入FIN_WAIT_1状态,服务端收到FIN之后给对方进行ACK,服务端会进入CLOSE_WAIT状态,客户端收到后进入FIN_WAIT_2状态。之后服务器发出FIN,状态变为LAST_ACK,客户端一收到FIN并且给对方ACK,客户端进入TIME-WAIT状态,服务端收到最后的ACK进入CLOSED状态。
CLOSE_WAIT时连接没关,因为四次回收没完成。如果服务器有大量的CLOSE_WAIT状态的连接的时候,原因是没有发FIN,即服务器引用层写的有BUG,忘了关闭对应的连接sockfd。
我们写一个简单的程序,并且不关闭sockfd
我们再用另一台机器访问
再查看状态,此时第一行local address并不是第一台服务器的公网IP地址,因为我们机器的公网IP并不一定是真实的,而是模拟出来的,现在图片里的IP才是真的IP。
我们让客户端主动断开(第二台机器充当客户端)。此时服务端的状态是CLOSE_WAIT,因为服务器没有关闭这个文件描述符,如果此时来了大量的连接,服务器上会充满大量的close——wait。
最后会导致服务器服务越来越差
四次挥手若正常完成,主动断开的一方会进入TIME_WAIT状态。
我们看到服务器主动断开连接(主动关闭了文件描述符),底层自动进行四次挥手,进入了TIME_WAIT
我们让服务器只获取连接
当连接上之后我们直接把服务干掉,由于文件描述符是跟随进程的,进程被关闭,文件描述符也随之被关。
此时自动触发了四次挥手,服务器成了主动断开连接的一方,服务器和客户端连接就处于TIME_WAIT状态。此时重新启动服务器,我们发现服务器无法重新启动,此时退出码是3
一段时间后8080端口服务已经没了
再重启服务器,此时才能重启成功。
我们根据Sock 代码,退出码为3代表绑定失败。
这是因为主动断开的一方要维持一个Timewait状态。 虽然四次挥手已经完成,但是主动断开连接的一方要维持一段时间的TIME_WAIT状态,在该状态下连接其实已经结束,但是地址信息ip,port依旧是被占用的。
即使服务器有time_wait状态,但TIME_WAIT状态不会再被使用, 服务器可以绕过TIME_WAIT的判断,直接让服务器绑定成功。
虽然是TIME_WAIT,但此时依旧可以重启
为什么主动断开的一方要处于TIME-WAIT?
当双方在协商断开连接时,可能会有一些报文滞留在网络中,还没有送达客户端或服务器,所以我们需要一个状态,来等等这些滞留在网络中的报文。这个时间一般是2倍的MSL(最大报文的生存时间),以确保历史数据从网络中消散,因为这些报文可能会影响之后建立的通信,而且等一段时间当主动断开的一方最后一次发出ACK时,如果ACK丢了,则对方会补发FIN,此时如果主动断开的一方不经过TIME_WAIT直接进入CLOSED状态,则会收不到对方的FIN,但有了TIME_WAIT之后,收到对方的FIN,继而重新发送ACK。
#pragma once
#include
#include
#include
#include
#include
// 日志是有日志级别的
#define DEBUG 0
#define NORMAL 1
#define WARNING 2
#define ERROR 3
#define FATAL 4
const char *gLevelMap[] = {
"DEBUG",
"NORMAL",
"WARNING",
"ERROR",
"FATAL"
};
#define LOGFILE "./threadpool.log"
// 完整的日志功能,至少: 日志等级 时间 支持用户自定义(日志内容, 文件行,文件名)
void logMessage(int level, const char *format, ...)
{
#ifndef DEBUG_SHOW
if(level== DEBUG) return;
#endif
// va_list ap;
// va_start(ap, format);
// while()
// int x = va_arg(ap, int);
// va_end(ap); //ap=nullptr
char stdBuffer[1024]; //标准部分
time_t timestamp = time(nullptr);
// struct tm *localtime = localtime(×tamp);
snprintf(stdBuffer, sizeof stdBuffer, "[%s] [%ld] ", gLevelMap[level], timestamp);
char logBuffer[1024]; //自定义部分
va_list args;
va_start(args, format);
// vprintf(format, args);
vsnprintf(logBuffer, sizeof logBuffer, format, args);
va_end(args);
//FILE *fp = fopen(LOGFILE, "a");
printf("%s%s\n", stdBuffer, logBuffer);
//fprintf(fp, "%s%s\n", stdBuffer, logBuffer);
//fclose(fp);
}
#pragma once
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include"Log.hpp"
class Sock
{
private:
const static int gbacklog=20;//一般不能太大也不能太小,后面会解释,这时listen的第二个参数
public:
Sock()
{}
int Socket()//创建套接字
{
int listensock=socket(AF_INET,SOCK_STREAM,0);//返回值照样是文件描述符
//参数含义第一个:网络通信 第二个:流式通信
if(listensock<0)//创建套接字失败
{
logMessage(FATAL,"create socket error,%d:%s",errno,strerror(errno));//打印报错信息
exit(2);
}
logMessage(NORMAL,"create socket success,sock:%d",listensock);//打印套接字,它的文件描述符是3
int opt=1;//标记位为打开状态
setsockopt(listensock,SOL_SOCKET,SO_REUSEADDR|SO_REUSEPORT,&opt,sizeof opt);
return listensock;
}
void Bind(int sock,uint16_t port,std::string ip="0.0.0.0")
{
//bind目的是让IP和端口进行绑定
//我们需要套接字,和sockaddr(这个里面包含家族等名称)
//绑定——文件和网络
struct sockaddr_in local;
memset(&local,0,sizeof local);//初始化local
local.sin_family=AF_INET;
local.sin_port=htons(port);//端口号
local.sin_addr.s_addr=ip.empty()?INADDR_ANY:inet_addr(ip.c_str());
//IP地址,由于我们构造的时候是IP是个空的字符串
//所以我们可以绑定任意IP
//我们一般推荐绑定0号地址或特殊IP
//填充的时候IP是空的,就用INADDR_ANY否则用inet_addr
if(bind(sock,(struct sockaddr*)&local,sizeof local)<0)
{
//走到这就绑定失败了,我们打印错误信息
logMessage(FATAL,"bind error,%d:%s",errno,strerror(errno));
exit(3);
}
}
void Listen(int sock)//将套接字设置为listen状态
{
//因为TCP是面向连接的,当我们正式通信的时候需要先建立连接。
if(listen(sock,gbacklog)<0)
{
logMessage(FATAL,"listen error,%d:%s",errno,strerror(errno));
exit(4);
}
logMessage(NORMAL,"init server success");
}
//一般经验
//const std;;string &::输入型参数
//std::string *:输出型参数
//std::string &:输入输出型参数
int Accept(int listensock,std::string *ip,uint16_t *port)//获取连接
{
//从套接字中获取到客户端相关的信息
struct sockaddr_in src;
socklen_t len=sizeof(src);
int servicesock=accept(listensock,(struct sockaddr*)&src,&len);
if(servicesock<0)
{
//获取连接失败
logMessage(ERROR,"accept error,%d:%s",errno,strerror(errno));
return -1;
}
if(port) *port=ntohs(src.sin_port);//如果port被设置,获取新的port
//客户端端口号在src
//由于是网络发送过来得套接字信息
//所以要把信息进行网络转主机
if(ip) *ip=inet_ntoa(src.sin_addr);//如果Ip被设置,获取新的Ip
//我们需要将四字节网络序列的IP地址,转换成字符串风格的点分十进制的IP地址
//到这里我们拿到了IP和端口号
return servicesock;
}
bool Connect(int sock, const std::string &server_ip, const uint16_t &server_port)//进行连接
{
struct sockaddr_in server;
memset(&server, 0, sizeof(server));
server.sin_family = AF_INET;
server.sin_port = htons(server_port);
server.sin_addr.s_addr = inet_addr(server_ip.c_str());
if(connect(sock, (struct sockaddr*)&server, sizeof(server)) == 0) return true;
else return false;
}
~Sock(){}
};
#include"Sock.hpp"
int main()
{
Sock sock;
int listensock=sock.Socket();
sock.Bind(listensock,8080);
sock.Listen(listensock);
while(true)
{
std::string clientip;
uint16_t clientport;
int sockfd=sock.Accept(listensock,&clientip,&clientport);
if(sockfd>0)
{
std::cout<<"["<