网络基础2

文章目录

将数据多变一的过程(方便网络发送和接受),称为序列化,将数据一变多的过程,称为反序列化(方便上层应用程序正常使用数据),如何序列化和反序列化是由协议规定的,而协议是根据应用场景所决定的

网络版本的计算器代码理解协议

// protocol.hpp 自定义协议

#pragma once

typedef struct request
{
	int x;
	int y;
	int op;
}request_t;


typedef struct response
{
	int code; // 0->success
	int result;
}response_t;

// server.cc

#include<iostream>
#include<string>
#include<cstring>
#include<unistd.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<arpa/inet.h>
#include<netinet/in.h>
#include<pthread.h>
#include"protocol.hpp"
#define BACKLOG 5

void* Routine(void* arg)
{
	pthread_detach(pthread_self());	
	int sock = *(int*)arg;
	while(1)
	{
		request_t rq;
		ssize_t size = recv(sock,&rq,sizeof rq,0);
		if(size > 0)
		{
				
		}
		else if(s == 0)
		{
			
		}
		else
		{

		}
	}
	close(sock);
	delete (int*)arg;
	return nullptr;	
}
// ./server port
int main(int argc,char* argv[])
{
	if(argc != 2)
	{
		std::cout<<"Usage : "<<argv[0]<<" port "<<std::endl; 
		exit(1);
	}

	int listen_sock = socket(AF_INET,SOCK_STREAM,0);
	if(listen_sock < 0)
	{
		exit(2);
	}

	struct sockaddr_in local;
	memset(&local,0,sizeof local);
	local.sin_family = AF_INET;
	local.sin_port = htons(atoi(argv[1]));
	local.sin_addr.s_addr = INADDR_ANY;

	if(bind(listen_sock,(struct sockaddr*)&local,sizeof local) < 0)
	{
		exit(3);
	}

	if(listen(listen_sock,BACKLOG) < 0)
	{
		exit(4);
	}


	while(1)
	{
		struct sockaddr_in peer;
		memset(&peer,0,sizeof peer);
		socklen_t len = sizeof peer;
		int sock = accept(listen_sock,(struct sockaddr*)&peer,&len);
		if(sock < 0)
		{
			continue;
		}
		pthread_t tid;
		int* p = new int(sock);
		pthread_create(&tid,nullptr,Routine,p);
	}
}

// client.cc

#include<iostream>
#include<string>
#include<cstring>
#include<unistd.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<arpa/inet.h>
#include<netinet/in.h>

// ./client server_ip server_port
int main(int argc,char* argv[])
{
	if(argc != 3)
	{
		std::cout<<"Usage : "<<argv[0]<<" server_ip server_port "<<std::endl; 
		exit(1);
	}

	int sock = socket(AF_INET,SOCK_STREAM,0);
	if(sock < 0)
	{
		exit(2);
	}

	struct sockaddr_in peer;
	memset(&peer,0,sizeof peer);
	peer.sin_family = AF_INET;
	peer.sin_addr.s_addr = inet_addr(argv[1]);
	peer.sin_port = htons(atoi(argv[2]));

	if(connect(sock,(struct sockaddr*)&peer,sizeof peer) < 0)
	{
		exit(3);
	}

	while(1)
	{
		request_t rq;
		std::cout<<"输入第一个数据 : ";
		cin>>rq.x;
		std::cout<<"输入第二个数据 : ";
		cin>>rq.y;
		std::cout<<"输入操作符 : ";
		cin>>rq.op;
		
		send(sock,&rq,sizeof rq,0);

		response_t rsp;
		recv(sock,&rsp,sizeof rsp,0);

		std::cout<<"status : "<<rsp.code<<std::endl;
		std::cout<<rq.x<<rq.op<<rq.y<<"="<<rsp.result<<std::endl;

	}	
}
#include <sys/types.h>
#include <sys/socket.h>
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
flags : 设置发送方式,设置为0为阻塞式发送
返回值为实际发送的字节数
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
flags : 设置发送方式,设置为0为阻塞式接受
返回值为实际读取的字节数

HTTP协议
在这里插入图片描述

说明 :
(1). 标识机器我们用的是公网IP,而IP不适合给人看,所以使用域名的方式
(2). 服务和端口的对应关系是明确的(http协议对应的端口号就是80)
(3). http协议本质是要获得某种资源(视频,音频,网页,图片)
(4). 登录信息和端口号可以省略
(5). 上网的大部分行为,都存在着进程间通信,大部分的上网行为,无非2种,把本地的数据推送到服务器(搜索/注册/登录/下单),把服务器上的资源数据拿到本地(短视频/网络小说)

urlencode和urldecode

像 / ? : 等这样的字符, 已经被url当做特殊意义理解了. 因此这些字符不能随意出现.
比如, 某个参数中需要带有这些特殊字符, 就必须先对特殊字符进行转义.
转义的规则如下:
将需要转码的字符转为16进制,然后从右到左,取4位(不足4位直接处理),每2位做一位,前面加上%,编码成%XY

http是基于请求与响应的应用层服务,常规情况下,http(https)底层使用的传输层协议是 tcp

#include<iostream>
#include<arpa/inet.h>
#include<netinet/in.h>
#include<sys/socket.h>
#include<sys/types.h>
#include<cstring>
#include<unistd.h>
#include<sys/wait.h>
#define BACKLOG 5

int main()
{
	int listen_sock = socket(AF_INET,SOCK_STREAM,0);
	if(listen_sock < 0)
	{
		perror("listen_sock\n");
		return 1;
	}

	struct sockaddr_in local;
	memset(&local,0,sizeof local);
	local.sin_family = AF_INET;
	local.sin_port = htons(8081);	
	local.sin_addr.s_addr = INADDR_ANY;

	if(bind(listen_sock,(struct sockaddr*)&local,sizeof local) < 0)
	{
		perror("band\n");
		return 2;
	}	

	if(listen(listen_sock,BACKLOG) < 0)
	{
		perror("listen\n");
		return 3;
	}

	while(1)
	{
		struct sockaddr_in peer;
		memset(&peer,0,sizeof peer);
		socklen_t len = sizeof peer;
		int sock = accept(listen_sock,(struct sockaddr*)&peer,&len);
		if(sock < 0)
		{
			std::cout<<"accept error"<<std::endl;
			continue;
		}
		if(fork() == 0)
		{
			close(listen_sock);
			if(fork() > 0) exit(0);
			// read http request
			char buffer[1024];
			recv(sock,buffer,sizeof buffer,0);
			std::cout<<"################### http request begin ##########################"<<std::endl;
			std::cout<<buffer<<std::endl;
			std::cout<<"################### http request end   ##########################"<<std::endl;
			exit(0);
		}
		close(sock);
		waitpid(-1,nullptr,0);
	}
}

写完上述简单的tcp服务器,我们可以在浏览器中输入IP地址和端口号,服务器就会收到http请求
或使用 post man 给服务器发送http请求

在这里插入图片描述

使用 post man 发送http请求(GET方法)
在这里插入图片描述

在浏览器中输入 IP地址 + 端口号 + /a/b/c/d.html(资源路径),服务器收到如下请求
或使用 post man 给服务器发送http请求

在这里插入图片描述

使用 post man 发送http请求(GET方法)
在这里插入图片描述

http请求格式 :

请求行 : 请求方法 url http协议版本(客户端版本)
请求报头 : key: value
key: value

(请求的相关属性,如是否需要长链接,浏览器的相关信息等等)
空行
请求正文(非必须)

说明 :
(1). 请求行,请求报头,空行全部是按行方式陈列的
(2). 请求正文一般是用户的相关信息或者数据,当我们登录注册时,会把我们的用户名,密码信息放到正文
(3). 服务器端在收到 http 请求后,可以按行进行循环读取,一直读到空行,就证明已经把请求报头读完了,所以 http 请求空行的含义是将报头和有效载荷进行分离的特殊符号

请求方法 : 常见 GET POST
GET : 获取资源,也有可能上传数据(百度搜索时可以看到搜索关键字)
POST : 上传数据,参数不会被同步到 url 当中,而是通过正文发送
在这里插入图片描述

url : 想访问的资源路径

http响应格式 :
状态行 : http协议(服务器端版本) 状态码 状态码描述
响应报头 : key: value
key: value

空行
响应正文(html/图片/音频等资源)(非必须)

客户端在收到 http 响应后,可以按行进行循环读取,一直读到空行,就证明已经把响应报头读完了

为什么要提供客户端和服务器端的http协议版本?
为了解决兼容性的问题, 客户端可能使用老的版本,服务器可能使用新的版本,服务器根据客户端的版本提供对应的服务
状态码 : 请求的结果(服务器把请求的事情做的怎么样)

telnet www.baidu.com 80
可以得到百度服务器的响应

在这里插入图片描述

GET 方法 :
(1). 直接获取对应的资源信息,比如网页!
(2). GET可以带参数(参数在 url 后面),在 url 将信息传递给服务器

https://cn.bing.com/search?
// 参数
q=%E6%80%AA%E5%BD%A2&cvid=8ace6f76d2c14d5ba5f36899f1c292de&aqs=edge.7.69i59i450l8.54419j0j1&FORM=ANNTA1&PC=U531

POST 方法 :
将数据提交给服务器,通过正文提交(不通过 url 传参)

在这里插入图片描述

GET和POST如果传参,GET通过url,POST通过正文,因此 POST 比 GET 能够传递更多的数据/更私密

http的特点 :
(1). 无状态的(第一次请求/第二次请求/…没有关系)

http 状态码

在这里插入图片描述

3XX重定向
301永久移动(301 Moved Permanently)
302临时移动(307 Moved Temporarily)
307临时移动(307 Moved Temporarily)

#include<iostream>
#include<arpa/inet.h>
#include<netinet/in.h>
#include<sys/socket.h>
#include<sys/types.h>
#include<cstring>
#include<unistd.h>
#include<sys/wait.h>
#define BACKLOG 5

int main()
{
	int listen_sock = socket(AF_INET,SOCK_STREAM,0);
	if(listen_sock < 0)
	{
		perror("listen_sock\n");
		return 1;
	}

	struct sockaddr_in local;
	memset(&local,0,sizeof local);
	local.sin_family = AF_INET;
	local.sin_port = htons(8080);	
	local.sin_addr.s_addr = INADDR_ANY;

	if(bind(listen_sock,(struct sockaddr*)&local,sizeof local) < 0)
	{
		perror("band\n");
		return 2;
	}	

	if(listen(listen_sock,BACKLOG) < 0)
	{
		perror("listen\n");
		return 3;
	}

	while(1)
	{
		struct sockaddr_in peer;
		memset(&peer,0,sizeof peer);
		socklen_t len = sizeof peer;
		int sock = accept(listen_sock,(struct sockaddr*)&peer,&len);
		if(sock < 0)
		{
			std::cout<<"accept error"<<std::endl;
			continue;
		}
		if(fork() == 0)
		{
			close(listen_sock);
			if(fork() > 0) exit(0);
			// read http request
			char buffer[1024];
			recv(sock,buffer,sizeof buffer,0);
			std::cout<<"################### http request begin ##########################"<<std::endl;
			std::cout<<buffer<<std::endl;
			std::cout<<"################### http request end   ##########################"<<std::endl;
			

			std::string status_line = "http/1.1 307 Temporary Redirect\n";
			std::string response_header = "Content-Length: 40\n";
			response_header += "location: https://www.qq.com/\n";
			std::string blank = "\n";

			send(sock,status_line.c_str(),status_line.size(),0);
			send(sock,response_header.c_str(),response_header.size(),0);
			send(sock,blank.c_str(),blank.size(),0);

			exit(0);
		}
		close(sock);
		waitpid(-1,nullptr,0);
	}
}

上述代码在浏览器中输入IP地址加端口号就可以跳转到腾讯的官网

Cookie: 用于在客户端存储少量信息. 通常用于实现会话(session)的功能
(当我们登录一个网站时,会输入账号和密码,短时间内再次登录,就不需要再输入账号和密码)
当第一次输入账号和密码时,账号和密码会在服务器端进行认证,服务器认证成功后,会把 Set-Cookie: 用户数据 响应回浏览器,浏览器识别到用户数据后会保存到浏览器的Cookie文件中,第二次发送登录请求时会发送 Cookie: 用户数据 字段给服务器,从而不用再次输入账号和密码,但 Cookie 文件容易被别人窃取,因此并不安全

当第一次输入账号和密码时,账号和密码会在服务器端进行认证,服务器认证成功后,会生成一个唯一的 session_id ,把 Set-Cookie: session_id 响应回浏览器,浏览器识别后会保存到浏览器的Cookie文件中,第二次发送登录请求时会发送 Cookie: session_id 字段给服务器,服务器确认后从而不用再次输入账号和密码

https加密流程:
在这里插入图片描述

http/1.0 是基于 request 和 response 方式的(一次请求,一次响应,然后断开链接),但这样太浪费资源,所以 http/1.1 支持长链接,客户端可以向服务器同时发送多个请求

传输层
http等上层协议,本质其实不是把请求和响应发送给网络,而是把自己的数据给了下层协议(TCP)

(1). 一个进程是否可以bind多个端口号?
可以
(2). 一个端口号是否可以被多个进程bind?
不可以,一个端口号只能唯一标识一个进程

iostat
用于输出磁盘IO 和 CPU的统计信息
-c: 显示CPU使用情况
-d: 显示磁盘使用情况
-N: 显示磁盘阵列(LVM) 信息
-n: 显示NFS 使用情况
-k: 以 KB 为单位显示
-m: 以 M 为单位显示
-t: 报告每秒向终端读取和写入的字符数和CPU的信息
-V: 显示版本信息
-x: 显示详细信息
-p:[磁盘] 显示磁盘和分区的情况

pidof[进程名]
功能: 通过进程名查看进程id

UDP协议(用户数据报协议)
UDP协议本质上是内核中的一种数据结构,可以定制UDP报头并添加UDP数据
在这里插入图片描述

UDP协议简介:
(1). 16位源端口号,16号目的端口号
在 socket 编程中,使用 sendto 系统调用时会填充 struct sockaddr_in 结构体,将填充的结构体中的端口号信息交给udp协议
(2). 16位UDP长度
整个UDP报文的长度
(3). 16位UDP检验和
发送前检验和 和 发送到对方后检验和一样说明数据在网络传输中没有发生过错误

说明 :
(1). UDP如何保证报头和有效载荷分离?
UDP协议采用定长报头
(2). UDP如何决定自己的有效载荷交给上层哪个协议?
UDP协议通过目的端口号可以将有效载荷交给上层的特定协议
(3). 什么叫做报头?
UDP协议属于内核协议栈(C语言),用C语言如何表示报头呢?使用位段!

struct udp_header
{
	unsigned int src_port:16;
	unsigned int dst_port:16;
	unsigned int udp_len:16;
	unsigned int udp_chk:16;
};

使用 sendto 发送数据时,操作系统在内核中创建一个udp报头,将报头的信息填充好,再把数据拷贝进来,这就是udp报文(前半部分是位段结构体变量,后半部分是数据)

(4). 传输层是如何通过端口,找到对应的服务进程的?
操作系统采用哈希方式,将端口号和pid映射起来
在这里插入图片描述

UDP的特点
UDP传输的过程类似于寄信.
(1). 无连接: 知道对端的IP和端口号就直接进行传输, 不需要建立连接;
(2). 不可靠: 没有确认机制, 没有重传机制; 如果因为网络故障该段无法发到对方, UDP协议层也不会给应用层返回任何错误信息;
(3). 面向数据报: 不能够灵活的控制读写数据的次数和数量,应用层交给UDP多长的报文, UDP原样发送, 既不会拆分, 也不会合并,如果发送端调用一次sendto, 发送100个字节, 那么接收端也必须调用对应的一次recvfrom, 接收100个字节; 而不能循环调用10次recvfrom, 每次接收10个字节;

UDP的缓冲区
UDP没有真正意义上的 发送缓冲区. 调用sendto会直接交给内核, 由内核将数据传给网络层协议进行后续的传输动作;
UDP具有接收缓冲区. 但是这个接收缓冲区不能保证收到的UDP报的顺序和发送UDP报的顺序一致; 如果缓冲区满了, 再到达的UDP数据就会被丢弃;

TCP协议
TCP全称为 “传输控制协议(Transmission Control Protocol”). 人如其名, 要对数据的传输进行一个详细的控制

TCP是保证可靠性的协议,UDP是不保证可靠性的协议,那为什么还要使用UDP协议呢?
TCP协议保证可靠性就意味着TCP需要做更多的工作,成本和复杂度就更高,如支付/私密通信
UDP协议不保证可靠性,但更简单,通常用在可以容忍一定丢包的情况下,如直播/视频/域名解析

什么是真正的可靠?
一般而言,只有我发的消息收到了响应,才算我的消息可靠的被对方收到(确认应答机制),所以我们可以认为在最新的消息之前都可以认为是真正的可靠的

在这里插入图片描述

TCP协议简介:
(1). 4位TCP报头长度: 表示该TCP头部有多少个32位bit(有多少个4字节); 所以TCP头部最大长度是15 * 4 = 60
(2). 32位序号 : 当 client 端发送一批TCP报文给 server 端时,会填写该报文的序号,server 端可以根据报文的32位序号进行顺序重排,放在TCP的接受缓冲区中,从而保证发送接受报文的有序
(3). 32位确认序号(n) : 是对应的32位序号 + 1,告知对方发送的n个字节之前的内容已经全部收到,下次发送请从n开始发送
(4). 16位窗口大小 : 填充的是自身接收缓冲区中剩余空间的大小
(5). 16位校验和 : 对整个TCP报文进行校验
(6). 6个标志位(一个标志位占一个比特位)
URG: 紧急指针是否有效
ACK: 确认号是否有效
PSH: 提示接收端应用程序立刻从TCP缓冲区把数据读走
在这里插入图片描述

RST: 对方要求重新建立连接; 我们把携带RST标识的称为复位报文段
在这里插入图片描述

SYN: 请求建立连接; 我们把携带SYN标识的称为同步报文段
FIN: 通知对方, 本端要关闭了, 我们称携带FIN标识的为结束报文段
(7). 16位紧急指针 : 紧急数据在报文中的偏移量,标识哪部分数据是紧急数据(紧急数据只能有1字节内容)
在这里插入图片描述

在 recv 中的flags选项中有MSG_OOB选项用来读取紧急数据
在这里插入图片描述

说明:
(1). TCP如何保证报头和有效载荷分离?
TCP报头标准大小为20字节,带上选项最大为60字节,因此先读取20字节,从中提取出4位首部长度的大小,计算出TCP报头的大小,从而将报头和有效载荷分离
(2). TCP如何决定自己的有效载荷交给上层哪个协议?
TCP协议通过目的端口号可以将有效载荷交给上层的特定协议
(3). 当client发送一批数据时,server端是怎么识别中间是否有缺失呢?
client发送一批数据,序列号分别为 1000,2000,3000,但 server 发现只有 1000 和 3000,所以会响应1001,让 client 重传第二份数据
(4). 为什么要用两套序号机制?
因为tcp是全双工的,client端有client序号和对server的确认,server端有server序号和对client的确认
(5). tcp是有接受和发送缓冲区的
tcp需要将发送出去的数据暂时缓存起来(发送缓冲区),因为数据有可能会重传,因此 write/send 这些接口并不是把数据直接发送到网络中,而是把用户数据copy到发送缓冲区中
上层应用来不及读走的数据会被暂时缓存起来(接受缓冲区),因此 read/recv 并不是直接从网络中读取,而是从接受缓冲区copy数据到用户层
用户层将数据copy到发送缓冲区,tcp从发送缓冲区里取数据进行发送(生产者消费者模型)
(6). 如果 client 端发送数据快, server 端接收数据慢,就有可能把接受缓冲区写满,丢弃一些正确的报文,所以 server 端响应时需要16位窗口大小告知 client 端自己接收缓冲区的剩余大小,从而 client 端就可以控制自己的发送速度(16位窗口大小支持流量控制)

(7). tcp报文是有种类的,有些报文发送正常数据,有些报文要建立链接,有些报文要断开链接,有些是确认报文,所以6个标志位标志了tcp报文的种类
(8). 三次握手/四次挥手(SYN/FIN)
在这里插入图片描述

在这里插入图片描述

(9). 如何理解链接,如何理解建立和断开链接?
server : client = 1 : n,存在很多的客户端链接 server ,OS需要管理这些链接(先描述,再组织),因此双方建立链接成功后需要创建出维护链接的结构体,再使用数据结构将结构体组织起来,断开链接就是释放曾经建立好的链接结构体

(10). 如果一个数据需要被提前读取,URG + 16位紧急指针完成

(11). client 端认为只要最后一个ACK发出,自己就建立好了链接,server 端认为自己接收到ACK时,链接建立成功,而报文在网络上传送需要花费时间,所以 client 和 server 认为链接建立成功是有一点时间差的,那就会存在这样一种情况 : ACK在传送过程中丢失了,导致 client 认为链接建立成功,server 认为链接建立不成功,client 认为链接建立成功,开始给 server 发送数据,但 server 认为链接建立不成功,server 的tcp响应报文中将 RST 置 1,要求重新建立链接

超时重传机制
TCP保证可靠性,一方面通过TCP报头体现的,另一方面是通过TCP的代码逻辑体现的,超时重传机制在TCP报头中无法体现,是在TCP的代码逻辑中体现的

在这里插入图片描述

那么, 如果超时的时间如何确定?
最理想的情况下, 找到一个最小的时间, 保证 "确认应答一定能在这个时间内返回,但是这个时间的长短, 随着网络环境的不同, 是有差异的,如果超时时间设的太长, 会影响整体的重传效率,如果超时时间设的太短, 有可能会频繁发送重复的包

TCP为了保证无论在任何环境下都能比较高性能的通信, 因此会动态计算这个最大超时时间,Linux中(BSD Unix和Windows也是如此), 超时以500ms为一个单位进行控制, 每次判定超时重发的超时时间都是500ms的整数倍.如果重发一次之后, 仍然得不到应答, 等待 2500ms 后再进行重传.如果仍然得不到应答, 等待 4500ms 进行重传. 依次类推, 以指数形式递增.累计到一定的重传次数, TCP认为网络或者对端主机出现异常, 强制关闭连接

连接管理机制

(1). 为什么建立链接要3次握手?
因为tcp是全双工协议,链接建立的核心要务 : 首先要验证双方的通信信道是连通的(即 client 和 server 即能发送报文也能接收报文),而3次握手是验证双方通信信道的最少次数
在这里插入图片描述

3次握手,在链接异常的情况下,已经建立的链接一定是在 client 端(因为前两次握手有超时重传机制保证,链接由最后一次发送ACK的来维护),如果是偶数次握手,最后的异常链接一定是在 server 端,而维护链接是需要消耗时间和空间的,就有可能导致操作系统短期内无法再建立和接收新的链接
在这里插入图片描述

(2). 为什么断开链接要四次挥手?
因为tcp是全双工协议,所以断开链接需要双方的同意,两次挥手可以单方向的断开链接,4次挥手就可以完成双方信道的关闭

断开链接时双方所处的状态
(1). 首先发起 FIN 请求的 client 端处于 FIN_WAIT_1
(2). server 收到 client 的 FIN 请求并给 client 响应后,server 处于 CLOSE_WAIT(半关闭状态)
(3). client 端收到 sever 的响应后处于 FIN_WAIT_2
(4). server 向 client 发起 FIN 请求,处于 LAST_ACK(最后确认)
(5). client 收到 server 的 FIN 请求并给 server 响应后,client 处于 TIME_WAIT
(6). server 端收到 client 的响应后处于 CLOSE
(7). client 端过一段时间处于 CLOSE

在这里插入图片描述

在这里插入图片描述

说明 :
(1). 在 socket 编程中,如果只有 client 端 close(fd),server 端会存在处于 CLOSE_WAIT 状态的链接,造成链接资源没有释放完全(如果发现server端存在大量的CLOSE_WAIT状态,需要排查是否没有及时close(fd))
(2). 为什么主动发起断开链接的一端(client 端)发出响应后,要过一段时间才会 CLOSE 呢?
如果 client 端发出响应后直接关闭,如果发出的响应在传输过程中丢失了,server 端会重传,但此时 client 端已经关闭,不能在发出响应,会导致 server 端不断重传
保证双方通信信道上面的正常数据在网络中尽可能的消散
(3). 断开链接是不一定能成功
(4). TIME_WAIT 等待时间是多少?
为 2MSL,MSL : 单向数据通信的最长时间
MSL

通过以下代码可以帮助我们看到客户端和服务器的各种状态

#include<iostream>
#include<sys/types.h>
#include<sys/socket.h>
#include<arpa/inet.h>
#include<netinet/in.h>
#include<unistd.h>

const int port = 8081;
const int num = 5;

void* Routine(void* arg)
{
	int sock = *(int*)arg;
	pthread_detach(pthread_self());
	while(1)
	{
		std::cout<<"thread is running "<<pthread_self()<<std::endl;
		sleep(1);
	}
	delete (int*)arg;
	return nullptr;
}
int main()
{
	int listen_sock = socket(AF_INET,SOCK_STREAM,0);
	if(listen_sock < 0)
	{
		perror("socket\n");
		return 1;
	}
	// int opt = 1;
	// setsockopt(listen_sock,SOL_SOCKET,SO_REUSEADDR,&opt,sizeof opt);
	struct sockaddr_in local;
	local.sin_family = AF_INET;
	local.sin_port = htons(port);
	local.sin_addr.s_addr = INADDR_ANY;

	if(bind(listen_sock,(struct sockaddr*)&local,sizeof local) < 0)
	{
		perror("bind\n");
		return 2;
	}

	if(listen(listen_sock,num) < 0)
	{
		perror("listen\n");
		return 3;
	}
	
	struct sockaddr_in peer;
	socklen_t len = sizeof(peer);
	while(1)
	{
		int sock = accept(listen_sock,(struct sockaddr*)&peer,&len);
		if(sock < 0)
		{
			perror("accept\n");
			continue;
		}

		std::cout<<"got a new link"<<sock<<std::endl;
		int *p = new int(sock);
		pthread_t tid;
		pthread_create(&tid,nullptr,Routine,p);
	}
}

监控脚本

while :; do sudo netstat -ntp | grep 8081 ; sleep 1; echo "#######"; done

先启动 server ,运行起监控脚本,使用 telnet 127.0.0.1 8081 命令连接服务器,三次握手建立连接
在这里插入图片描述

(1). 先关闭客户端,由于测试代码中没有 close(sock),所以只有两次挥手,客户端处于FIN_WAIT_2状态,服务器会处于CLOSE_WAIT状态,最后服务器进程退出,完成两次挥手,客户端处于TIME_WAIT状态,服务器会处于CLOSE状态
在这里插入图片描述
在这里插入图片描述

(2). 先关闭服务器端,服务器端最后会处于 TIME_WAIT 状态,此时再次启动服务器会出现绑定失败的错误,因为服务器端连接并没有完全断开
在这里插入图片描述

(3). 可以使用 setsockopt 接口来解决TIME_WAIT状态引起的bind失败

#include <sys/types.h>     
#include <sys/socket.h>

int setsockopt(int sockfd, int level, int optname,const void *optval, socklen_t optlen);

滑动窗口(传输效率角度,提高传送效率)
滑动窗口大小指的是无需等待确认应答而可以继续发送数据的最大值
既然 tcp 发送方可以一次发送多个 tcp 段,可不可以一次就把数据发完呢?
这是不可以的,因为还需考虑到对方的接收能力

在这里插入图片描述

(1). 窗口大小表示的是对方的接收能力,衡量的是对方的接收缓冲区剩余空间的大小
(2). 滑动窗口 : 在自己的发送缓冲区中限定的一块区域,数据可以直接发送不用等待ACK
(3). 滑动窗口不一定会整体右移,如果接收方的上层一直不拿走数据,窗口大小会一直减小
(4). 在没有收到应答的时候,滑动窗口的数据不会被删除(支持超时重传)
在这里插入图片描述

快重传 :
情况一: 数据包已经抵达, ACK被丢了
这种情况下, 部分ACK丢了并不要紧, 因为可以通过后续的ACK进行确认
在这里插入图片描述

情况二 : 数据包就直接丢了
在这里插入图片描述

当某一段报文段丢失之后, 发送端会一直收到 1001 这样的ACK, 就像是在提醒发送端 “我想要的是 1001” 一样,如果发送端主机连续三次收到了同样一个 “1001” 这样的应答, 就会将对应的数据 1001 - 2000 重新发送,这个时候接收端收到了 1001 之后, 再次返回的ACK就是7001了(因为2001 - 7000)接收端其实之前就已经收到了, 被放到了接收端操作系统内核的接收缓冲区中

(4). 快重传 vs 超时重传
在数据包丢失的情况下,快重传机制只有 client 端收到3次相同报文的时候才会被触发,不够3次会使用超时重传机制

流量控制(可靠性角度,防止接收端接收缓冲区慢导致大量丢包)
接收端处理数据的速度是有限的. 如果发送端发的太快, 导致接收端的缓冲区被打满, 这个时候如果发送端继续发送,就会造成丢包, 继而引起丢包重传等等一系列连锁反应,因此TCP支持根据接收端的处理能力, 来决定发送端的发送速度. 这个机制就叫做流量控制(Flow Control);

(1).接收端将自己可以接收的缓冲区大小放入 TCP 首部中的 “窗口大小” 字段, 通过ACK端通知发送端,
(2). 窗口大小字段越大, 说明网络的吞吐量越高;
(3). 接收端一旦发现自己的缓冲区快满了, 就会将窗口大小设置成一个更小的值通知给发送端;
(4). 发送端接受到这个窗口之后, 就会减慢自己的发送速度;
怎么减慢发送端的发送速度呢?
通过滑动窗口(start = ack确认号,end = start + tcp_win)
(5). 如果接收端缓冲区满了, 就会将窗口置为0; 这时发送方不再发送数据, 但是需要定期发送一个窗口探测数据段, 使接收端把窗口大小告诉发送端.
(6). 第一次发送数据时,A如何得知对方的接收缓冲区大小呢?
在三次握手阶段,就已经协商好了窗口的大小

拥塞控制
虽然TCP有了滑动窗口这个大杀器,能够高效可靠的发送大量的数据. 但是如果在刚开始阶段就发送大量的数据,仍然可能引发问题(如出现大量丢包问题),说明网络出现了拥塞问题,当网络拥塞时,我们是不能采用重传的策略的,这会使网络情况雪上加霜,正确做法是尽量不发数据或者少发数据,等待网络状况恢复

拥塞窗口
在两台主机进行通信的过程中,可能双方的16位窗口大小和滑动窗口大小很大,但网络情况并不好,此时如果发送数据过多就可能会导致网络拥塞的问题,由此引出拥塞窗口的概念
一次发送的数据量,大于拥塞窗口,可能会引发网络拥塞问题
说明 :
(1). 滑动窗口大小 = min(对方的接收能力,自己拥塞窗口大小)
(2). 拥塞窗口大小随网络状态的变化而改变
(3). 每台主机都有拥塞窗口,且拥塞窗口大小不一定一样
(4). 检测到网络发生拥塞,采用慢启动算法,先发送少量数据确认网络是否畅通,然后再尽快的恢复通信速度(先指数再线性)

TCP引入慢启动机制, 先发少量的数据, 探探路, 摸清当前的网络拥堵状态, 再决定按照多大的速度传输数据

拥塞窗口增长速度, 是指数级别的. “慢启动” 只是指初使时慢, 但是增长速度非常快,为了不增长的那么快, 因此不能使拥塞窗口单纯的加倍,此处引入一个叫做慢启动的阈值,当拥塞窗口超过这个阈值的时候, 不再按照指数方式增长, 而是按照线性方式增长
在这里插入图片描述

当TCP开始启动的时候, 慢启动阈值等于窗口最大值;
在每次超时重发的时候, 慢启动阈值会变成原来的一半, 同时拥塞窗口置回1;

面向字节流 :
(1). 写100个字节数据时, 可以调用一次write写100个字节, 也可以调用100次write, 每次写一个字节;
(2). 读100个字节数据时, 也完全不需要考虑写的时候是怎么写的, 既可以一次read 100个字节, 也可以一次read一个字节, 重复100次;
(3). 例如,client 端发送 http 请求时,将应用层的数据拷贝到 client 的发送缓冲区中(怎么拷贝都可以),发送到 server 端的接收缓冲区(怎么读取都可以),tcp也不会关心数据的内容是什么,只需要把数据发出去就好了,怎么解释收到的数据由 http 协议决定
(4). 基于tcp协议的应用层可能会存在粘包问题

粘包问题
例如 : client 端将 http 请求交给 server 端的接收缓冲区,上层识别时根据 http 协议进行识别,但上层并没有根据 http 协议进行读取,而是通过 read 或 recv 读取,就有可能多读或少读,这就叫做粘包问题(http 协议通过自描述 + 特殊字符解决粘包问题)

那么如何避免粘包问题呢? 归根结底就是一句话, 明确两个包之间的边界
(1). 定长报文
(2). 特殊字符,可以规定一些特殊字符来作为报文与报文的边界
(3). 自描述 + 定长,如udp协议先读取8字节,再根据16位udp长度读取数据,所以udp协议不存在粘包
(4). 自描述 + 特殊字符,如http中的空行,读到空行说明报头读完了,再根据 Content-Length 字段来读取正文

TCP异常情况 :
进程终止: 进程终止会释放文件描述符, 仍然可以发送FIN. 和正常关闭没有什么区别.
机器重启: 和进程终止的情况相同.
机器掉电/网线断开: 接收端认为连接还在, 一旦接收端有写入操作, 接收端发现连接已经不在了, 就会进行reset. 即使没有写入操作, TCP自己也内置了一个保活定时器, 会定期询问对方是否还在. 如果对方不在, 也会把连接释放.
另外, 应用层的某些协议, 也有一些这样的检测机制. 例如HTTP长连接中, 也会定期检测对方的状态. 例如QQ, 在QQ断线之后, 也会定期尝试重新连接

在这里插入图片描述

对于服务器, listen 的第二个参数设置为 2, 并且不调用 accept

#include<iostream>
#include<sys/types.h>
#include<sys/socket.h>
#include<arpa/inet.h>
#include<netinet/in.h>
#include<unistd.h>

const int port = 8081;
const int num = 2;

void* Routine(void* arg)
{
	int sock = *(int*)arg;
	pthread_detach(pthread_self());
	while(1)
	{
		std::cout<<"thread is running "<<pthread_self()<<std::endl;
		sleep(1);
	}
	delete (int*)arg;
	return nullptr;
}
int main()
{
	int listen_sock = socket(AF_INET,SOCK_STREAM,0);
	if(listen_sock < 0)
	{
		perror("socket\n");
		return 1;
	}
	// int opt = 1;
	// setsockopt(listen_sock,SOL_SOCKET,SO_REUSEADDR,&opt,sizeof opt);
	struct sockaddr_in local;
	local.sin_family = AF_INET;
	local.sin_port = htons(port);
	local.sin_addr.s_addr = INADDR_ANY;

	if(bind(listen_sock,(struct sockaddr*)&local,sizeof local) < 0)
	{
		perror("bind\n");
		return 2;
	}

	if(listen(listen_sock,num) < 0)
	{
		perror("listen\n");
		return 3;
	}
	
	struct sockaddr_in peer;
	socklen_t len = sizeof(peer);
	while(1)
	{
		sleep(1);
	}
}

客户端状态正常, 但是服务器端出现了 SYN_RECV 状态, 而不是 ESTABLISHED 状态这是因为, Linux内核协议栈为一个tcp连接管理使用两个队列:
(1). 半链接队列(用来保存处于SYN_SENT和SYN_RECV状态的请求)
(2). 全连接队列(accpetd队列)(用来保存处于established状态,但是应用层没有调用accept取走的请求)而全连接队列的长度会受到 listen 第二个参数的影响.
全连接队列满了的时候, 就无法继续让当前连接的状态进入 established 状态了.
这个队列的长度通过上述实验可知, 是 listen 的第二个参数 + 1.

为什么底层要维护链接队列呢?(不能没有,也不能太长)
当有链接断开时,服务器可以立马从链接队列中选取一个链接进行处理(保证服务器100%工作)
维护链接是有成本的,与其支持长链接,造成 client 端等待太久,并且大量占用暂时用不到的资源,不如把这些资源节省出来给 server 端使用

工作流程 : 三次握手->将半链接节点链到全链接->accept读取链接
在这里插入图片描述

如果大量用户只给 server 端发送 SYN,不会给 server 进行响应,server 端的半链接队列里会维持大量的废弃链接,全链接队列不会有建立好的链接,server 无法给 client 提供服务,这种攻击方式叫做SYN洪水攻击,大量的用户称之为肉鸡,为了解决这个问题,server 端会另外维护一个队列,client 端发送 syn ,server 端接收后响应报文中会携带syncookie(随机值),并将节点从半链接队列移到缓存队列中,若 client 没有给出响应就从缓存队列中丢弃,若给出响应且syncookie相同,从缓存队列中移到全链接队列中
在这里插入图片描述

TCP小结:
为什么TCP这么复杂?
因为要保证可靠性, 同时又尽可能的提高性能.

可靠性:
校验和
序列号(按序到达,去重)
确认应答
超时重发
连接管理
流量控制
拥塞控制

提高性能:
滑动窗口
快速重传
延迟应答
捎带应答

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值