c++ webserver/第四章 通信编程(下)

👍目录👈

1. socket

一系列相关API, 客户只需要提供(IP,端口号)即可进行通信

定义

对网络中不同主机上的应用进程之间进行双向通信的端点的抽象。提供应用层进程利用网络协议交换数据的机制。从所处的地位来讲,套接字上联应用进程,下联网络协议栈,是应用程序通过网络协议进行通信的接口,是应用程序与网络协议根进行交互的接口。

socket 是由 IP 地址和端口结合的,提供向应用层进程传送数据包的机制。

在 Linux 环境下,用于表示进程间网络通信的特殊文件类型。本质为内核借助缓冲区形成的伪文件。既然是文件,那么理所当然的,我们可以使用文件描述符引用套接字。与管道类似的,Linux 系统将其封装成文件的目的是为了统一接口,使得读写套接字和读写文件的操作一致。区别是管道主要应用于本地进程间通信,而套接字多应用于网络进程间数据的传递。

每个套接字文件都有:读/写缓存.

客户端:主动向服务器发起连接,

服务端;被动接受连接,一般不主动发起.

2.字节序

1.定义
  1. 字节序,顾名思义字节的顺序,就是大于一个字节类型的数据在内存中的存放顺序(一个字节的数据当然就无需谈顺序的问题了)。
  2. 字节序分为大端字节序(Big-Endian) 和小端字节序(Little-Endian)。大端字节序是指一个整数的最高位字节(23 ~ 31 bit)存储在内存的低地址处,低位字节(0 ~ 7 bit)存储在内存的高地址处;小端字节序则是指整数的高位字节存储在内存的高地址处,而低位字节则存储在内存的低地址处
  3. 字节 ----> 高地址–低地址
  4. 内存 ----> 低地址–高地址

所以大端模式是从第一位开始存,小端模式是从最后一位开始存,一般计算机采用小段字节序

2.存储和检测
//例子
0x0102030405060708;
//小段模式存储
0x08 | 0x07 |.....|0x01
//大端模式
0x01 | 0x02 |.....|0x08
    
//检测
#include<stdio.h>  
    
//union共享内存,任何数据都是从第一个地址开始.
union var{  
        //c[4]和i和l指向同一块内存地址
        char c[4]; 
        int i;
    	int l;
};  
  
int main(){  
	union var data;
	data.c[0] = 0x04;//因为是char类型,数字不要太大,算算ascii的范围~  
	data.c[1] = 0x03;//写成16进制为了方便直接打印内存中的值对比  
	data.c[2] = 0x02;
	data.c[3] = 0x11;

	data.c[4] = 0x11;//因为是char类型,数字不要太大,算算ascii的范围~  
	data.c[5] = 0x02;//写成16进制为了方便直接打印内存中的值对比  
	data.c[6] = 0x03;
	data.c[7] = 0x04;

	printf("%x\n", data.i);		//11020304
	printf("%x\n", data.l);		//11020304

	data.i = 5;
	printf("%x\n", data.l);		//5 
}  
//出现11020304为小端模式
3.字节序转换函数
  1. 发送端总是把要发送的数据转换成大端字节序数据后再发送,而接收端知道对方传送过来的数据总是采用大端字节序,所以接收端可以根据自身采用的字节序决定是否对接收到的数据进行转换(小端机转换,大端机不转换)
  2. 网络字节序固定:大端模式
//进行网络通信时,需要将主机字节序转换为网络字节序(小端化大端,大端不变!)
h  		-host 主机,主机字节序;
to  	-转换目标;
n		-network 网络,网络字节序;
s		-short(unsigned short)	 	指明类型,2个字节(端口);unsigned short int
l		-long(unsigned int)	 		指明类型,4个字节(IP);unsigned int

#include<arpa/inet.h>
//转换端口
uint16_t htons(unit16_t hostshort);		//主机-网络
uint16_t ntohs(unit16_t netshort);		//网络-主机
//转换IP
uint32_t htonl(unit32_t hostlong);		//主机-网络
uint32_t ntohl(unit32_t netlong);		//网络-主机
4.测试
//例子
#include <stdio.h>
#include <arpa/inet.h>

int main()
{
    // htons 转换端口
    unsigned short port = 0x8081;

    unsigned short netport = htons(port);

    printf("%x\n", netport); // 8180

    // htonl 转换IP
    unsigned char ip[4] = {196, 168, 1, 1};
    unsigned int ipInt = *(int *)ip; //转化为int型
    unsigned int net = htonl(ipInt); //转化为网络字节序

    unsigned char *netip = (char *)&net; //输出
    printf("%d %d %d %d\n", *netip, *(netip + 1), *(netip + 2), *(netip + 3));

    return 0;
}

//补充
char c[6] = {1,1,1,1,1,1};
//char数组转化为int
int c = *(int *)a;
printf("%d\n",c);
//int转为char数组
int d = 10;
char* a = (char *)&d;

3. socket地址

//socket地址就是一个结构体,封装IP和端口号等信息.API的基础
//客户端 -> 服务器(ip,port);

//一下一般用IPV4, IPV6无法使用
typedef unsigned short int sa_family_t;
#include <bits/socket.h> struct sockaddr 
{ 
    //sa_family 成员是地址族类型(sa_family_t)的变量。
    sa_family_t sa_family;
    //地址详细数据,用于存放 socket 地址值。
    char sa_data[14];
};
1.参数详细
  1. 地址族类型通常与协议族类型对应。常见的协议族(protocol family,也称 domain PF)和对应的地址族入 下所示:

PF_*AF_* 都定义在 bits/socket.h 头文件中,且后者与前者有完全相同的值,所以二者通常混用。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-CfJhDntd-1646227978408)(第四章 通信编程.assets/image-20220302131652721.png)]

  1. 不同的协议族的地址值具有不同的含义和长度,如下所示

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gofXRjmd-1646227978409)(第四章 通信编程.assets/image-20220302132411361.png)]

2.优化,可以存放IPV6

Linux 定义了下面这个新的通用的 socket 地址结构体,这个结构体不仅提供了足够大的空间用于存放地址值,而且是内存对齐的。

#include <bits/socket.h> 

typedef unsigned short int sa_family_t;
struct sockaddr_storage 
{ 
    //地址族类型
    sa_family_t sa_family;
    //内存对齐使用
    unsigned long int __ss_align;
    //存储具体IP数据和端口号等.
    char __ss_padding[ 128 - sizeof(__ss_align) ];
};
3.专用的socket地址(现在使用)

所有专用 socket 地址(以及 sockaddr_storage)类型的变量在实际使用时都需要转化为通用 socket 地址类型 sockaddr**(强制转化即可)**,因为所有 socket 通信编程接口使用的地址参数类型都是 sockaddr。

sockaddr_in : IPV4

sockaddr_un: unix本地

sockaddr_in6: IPV6

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-G0VP5K7g-1646227978409)(第四章 通信通信编程.assets/image-20220302133429857.png)]

//unix专用
#include <sys/un.h> 
struct sockaddr_un 
{ 
    sa_family_t sin_family;
    char sun_path[108]; 
};


//定义
#define __SOCKADDR_COMMON_SIZE (sizeof (unsigned short int))
typedef unsigned short uint16_t;
typedef unsigned int uint32_t; 
typedef uint16_t in_port_t;
typedef uint32_t in_addr_t;

struct in_addr 
{ in_addr_t s_addr; };

//IPV4
#include <netinet/in.h>
struct sockaddr_in 
{
    sa_family_t sin_family; /* 协议族 */
    in_port_t sin_port; /* 端口. */ 
    struct in_addr sin_addr; /* IP地址 */
    unsigned char sin_zero[sizeof (struct sockaddr) - __SOCKADDR_COMMON_SIZE - sizeof (in_port_t) - sizeof (struct in_addr)]; };	//保留段


//IPV6
struct sockaddr_in6
{
    sa_family_t sin6_family; /* 协议族 */
    in_port_t sin6_port; /* 端口. */ 
    uint32_t sin6_flowinfo; /* 对齐 */ 
    struct in6_addr sin6_addr;/* IP地址 */
    uint32_t sin6_scope_id; /* IPv6 scope-id */ 
};

4. IP地址转化

1.旧版

字符串IP - 整数(还需要转化为网络字节序). 主机字节序 -> 网络字节序

//老版,一般不使用.只能用于IPV4
#include <arpa/inet.h>
//传入字符串,生成int型数据(二进制),!转化后的是网络字节序!
in_addr_t inet_addr(const char *cp);

//传入一个字符串,再传入一个 in_addr结构体,就可以
//把点分十进制的IP转化为二进制的网络字节序IP地址.并保存再inp结构体中
//返回值:1表示成功,0表示非法,值errno;
int inet_aton(const char *cp, struct in_addr *inp);

//传入一个 in_addr ,转化为一个点分十进制的字符串;
char *inet_ntoa(struct in_addr in);
2.新版

既可以用IPV4,也可以用IPV6即可

#include <arpa/inet.h> 
// p:点分十进制的IP字符串,n:表示network,网络字节序的整数

//将点分十进制的IP地址字符串,转换成网络字节序的整数
int inet_pton(int af, const char *src, void *dst); 
	af:地址族: AF_INET AF_INET6 
    src:需要转换的点分十进制的IP字符串 
    dst:转换后的结果保存在这个里面
//返回值:1表示成功,0表示非法. -1表示错误,置errno;
        
        
// 将网络字节序的整数,转换成点分十进制的IP地址字符串
// 字符串接受长度 16; char ip[16]; 点分十进制(每位3个) * 4 + 3个. + 结束符 = 16位;
const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);
	af:地址族:
    AF_INET AF_INET6 
    src:要转换的ip的整数的地址
    dst: 转换成IP地址字符串保存的地方 
    size:第三个参数的大小(数组的大小)s
 返回值:返回转换后的数据的地址(字符串),和 dst 是一样的
3.测试
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdio.h>
#include <stdlib.h>
int main()
{
    //点分十进制字符串转整数
    const char *str = "192.168.1.1";
    unsigned int num = 0;
    int ret = inet_pton(PF_INET, str, &num);
    if (ret <= 0)
    {
        perror("pton\n");
        exit(0);
    }
    printf("num = %d\n", num);		//16885952

    //整数转点分十进制字符串
    char res[16] = {0};
    const char *rec = inet_ntop(PF_INET, &num, res, sizeof(res));
    printf("ret = %s\n", rec);		//192.168.1.1
    printf("res = %s\n", res);		//192.168.1.1
    printf("str = %s\n", str);		//192.168.1.1
    return 0;
}

5. TCP通信

1. TCP 和 UDP
  1. 均为传输层协议
  2. UDP:用户数据报协议,面向无连接,可以单播,多播,广播,面向数据报,不可靠
    1. 不关心用户是否接受;
    2. 无拥塞控制,所以即使网络差也会以恒定速度发送;
  3. TCP:传输控制协议,面向连接的可靠的基于字节流(一端发送,另一端接受),仅支持单播传输
UDPTCP
是否创建连接无连接面向连接
是否可靠不可靠可靠的
连接的对象个数一对一、一对多、多对一 、多对多一对一
传输的方式面向数据报面向字节流
首部开销8个字节最少20个字节(头部)
适用场景实时应用(视频会议,直播)可靠性高的应用(文件传输)
2.通信流程
1.服务端
  1. ​ 创建一个监听的套接字
    • 监听: 监听客户端的连接
    • 套接字:本质是文件描述符
  2. 监听文件描述符绑定本地的 IP 和端口号
    • 客户端连接的时候使用的 IP 的端口号,需要固定
  3. 设置监听,即监听的fd开始工作
  4. 阻塞等待,直到有客户端发起连接,接触阻塞,接受客户端的连接. 连接完成可得到客户端的套接字(fd);
  5. 通信
    • 接收数据
    • 发送数据
  6. 通信结束,断开连接
2.客户端
  1. 创建一个套接字
  2. 连接服务器,指定服务器的IP 和 端口
  3. 连接成功,即可进行通信
    • 接收数据
    • 发送数据
  4. 通信结束,断开连接
3.流程图

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3g29U0c2-1646227978410)(第四章 通信编程.assets/image-20220302170444995.png)]

4.套接字函数
#include <sys/types.h> 
#include <sys/socket.h>
#include <arpa/inet.h> // 包含了这个头文件,上面两个就可以省略


// 功能:创建一个套接字
int socket(int domain, int type, int protocol);
/*
	参数:
	1.	domain:协议族
		AF_INET: IPV4
		AV_INET6: IPV6
		AV_UNIX,AF_LOCAL : 本地套接字
	2.	type:通信过程中使用的协议类型
		SOCK_STREAM  : 流式协议
		SOCK_DGRAM : 报式协议
	3.	protocol: 具体协议,一般写0;
		如果第二个参数写SOCK_STREAM,默认为TCP
		如果第二个参数写SOCK_DGRAM,默认为UDP
返回值:成功返回文件描述符,操作的就是内核缓冲区.失败返回 -1 置errno;
*/


//将文件描述符和 IP 和 端口号进行绑定,
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
/*
参数:
	sockfd : 通过socket函数获得的文件描述符
	addr: 绑定的socket地址,这个地址封装了 IP 和 端口号信息;
	addrlen: 第二个参数结构体占的内存大小
返回值:成功0.失败返回 -1 置errno;
*/


//监听这个socket上的连接
int listen(int sockfd, int backlog); // /proc/sys/net/core/somaxconn
/*
	sockfd :  通过socket函数获得的文件描述符
	backlog : 未连接和已经接和的最大值(线程最大值?),不需要太大(5).因为已连接后就要执行下recv(),那么就会-1;
返回值:成功0.失败返回 -1 置errno;
*/


//接收客户端连接,默认是一个阻塞的函数,阻塞等待客户端连接
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
/*
参数:
	sockfd: 通过socket函数获得的文件描述符;
	addr : 传出参数,记录连接成功后的客户端信息(ip,port)
	addrlen : 指定第二个参数对应的内存大小(是一个指针,需要在定义一个变量,用引用传递).
成功: 返回用于通信的文件描述符; 失败 -1置errno;
*/


//客户端连接服务器
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
/*
参数:
	sockfd : !!!用于通信的文件描述符(自己创建)!!!
	addr :   客户端要连接的服务器地址信息
	addrlen: 第二个参数的内存大小(变量)
返回值:成功0.失败返回 -1 置errno;
*/


ssize_t write(int fd, const void *buf, size_t count); // 写数据
ssize_t read(int fd, void *buf, size_t count); // 读数据
//如果读出来的数据长度为0,那么就断开连接!

6. TCP三次握手

1.目的

保证双方互相之间建立了连接

2.什么是连接

TCP 是一种面向连接的单播协议,在发送数据前,通信双方必须在彼此间建立一条连接。所谓的“连接”,其实是客户端和服务器的内存里保存的一份关于对方的信息,如 IP 地址、端口号等。

3.连接过程

三次握手是客户端发起的

1.连接全过程时序图

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JY8vLDn4-1646401901300)(第四章 通信编程.assets/image-20220303132223800.png)]

2.连接标志位变化(SYN,ACK)

连接的时候 SYN = 1.应答的时候 ACK = 1.如下图

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TpgWsau8-1646401901300)(第四章 通信编程.assets/image-20220303132526359.png)]

3. 头部信息变化

seq: 32为序号(代表当前发送的长度) 形式如: seq = num,

  • 第一次的时候序号是随机的.

ack : 代表接收方接受到的长度

  • 如果收到的是SYN, 就是在握手期间,ack = cseq + 1;

  • 如果收到字节信息, ack = cseq + num; 代表接受了num - 上一次接受的num 长度字节

  • 两端都有自己的 seq 和 ack. 不要混在一起;

流程大致如下. 后面如果服务器不发信息,那么客户端的 ack 均一致,只有遇到SYN和sseq时才增加

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-OGgWUSIj-1646401901301)(第四章 通信编程.assets/image-20220303134823331.png)]

4.例外,面试题.

为什么一定是三次握手,两次可以吗?四次呢?

  1. 注意: 当SYN=1时,是不带数据的
  2. 不行,因为第一次的时候连接时试探性的,只有服务器回复数据后才会发送自己的消息.第三次发送的就是客户端的消息数据;两次握手的情况下客户端无法确定自己能否接受消息.
  3. 客户端:第一次确定说明自己能发,收的时候确定说明自己能收;
  4. 服务器:第一次接受说明自己能收,再发出去就可以确定自己的发没问题;
  5. 四次没问题,最少为三次
4.全过程

第一次握手和第二次不可以带数据!!!连接还没建好

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-145KLJuq-1646401901302)(第四章 通信编程.assets/image-20220303135749066.png)]

7.滑动窗口

1.定义

滑动窗口协议是用来改善吞吐量的一种技术,即容许发送方在接收任何应答之前传送附加的包。接收方告诉发送方在某一时刻能送多少包称窗口尺寸)

窗口可以理解为缓冲区大小,大小会随着发送和接受数据变化.通信双方均有(发送缓冲区,接收缓冲区);

可以进行阻塞控制,把接受方的接受缓冲区置0即可

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-et6FAPxh-1646401901302)(第四章 通信编程.assets/image-20220303142818840.png)]

发送发的缓冲区:
	白色:空闲
    紫色:还没有发送的数据
    灰色:数据发送,还没确认
接收方的缓冲区:
	白色:空闲(根据空闲区来调节发送的速率)
    紫色:已经接收到的数据

8. TCP 四次挥手

1.定义

四次挥手,发生于断开连接时候. 在程序中调用了close()时使用TCP协议进行四次挥手;

发起方: 两端均可发起,谁先调用close谁先发起;

因为在TCP连接时候,采用三次握手连接时双向的,所以均可以挥手;

  • !不把 ACK和FIN一起发送,是因为服务端可能还有数据需要发送,得确认自己不想发送后再断开!
2.流程图

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-emKCu64U-1646401901303)(第四章 通信编程.assets/image-20220303145330749.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xmQKYqti-1646401901303)(第四章 通信编程.assets/image-20220303150031755.png)]

9.全流程

  1. SYN
  2. 0(0) : 随即序号(本次携带数据大小)
  3. win : windows(滑动窗口大小)
  4. mss : Maximum Segment Size(一条数据的最大数据量)
  5. FIN

注意:

  • 发送可以多次,然后服务端一次确认(3~8)
  • 在发送断开时,可以携带数据(13)
  • 记得回复的时候 遇到 SYN,FIN 需要把序号+1(14);\
  • 结束发送的一方不能发送数据,但可以接收数据(14~17)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-FgCaIYZH-1646401901303)(第四章 通信编程.assets/image-20220303143749922.png)]

10 TCP通信并发

//用多线程或者多进程来进行处理并发的任务
具体流程:
	1.	一个父进程,多个子进程
    2.	父进程负责等待并接受客户端的连接
    3.	子进程:完成通信,接受一个客户端连接,就创建一个子进程用于通信
软中断错误
//如果当前有阻塞进程,然后有另外有一个更高级的进程运行,那么就会使当前的进程产生错误
//错误为: errno = EINTR

11.状态转化图

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-UHUk1PQw-1646401901304)(第四章 通信编程.assets/image-20220304135743780.png)]

状态改变过程(简图)

握手

​ listen(listen)

syn_sent

​ syn_recv

established

​ established


挥手(可以交换)

FIN_WAIT_1(调用close())

​ CLOSE_WAIT(接到FIN)

​ LAST_ACK(调用close())

FIN_WAIT_2(接到ACK)

TIME_WAIT(接到FIN)

详细图

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nfzRJvIu-1646401901304)(第四章 通信编程.assets/image-20220304142019476.png)]

3.细节
1. TIME_WAIT状态

2MSL(Maximum Segment Lifetime)

主动断开连接的一方, 最后进出入一个 TIME_WAIT状态, 这个状态会持续: 2msl

msl: 官方建议: 2分钟, 实际是30s

原因

当 TCP 连接主动关闭方接收到被动关闭方发送的 FIN 和最终的 ACK 后,连接的主动关闭方必须处于TIME_WAIT 状态并持续 2MSL 时间。这样就能够让 TCP 连接的主动关闭方在它发送的 ACK 丢失的情况下重新发送最终的 ACK。主动关闭方重新发送的最终 ACK 并不是因为被动关闭方重传了 ACK(它们并不消耗序列号,被动关闭方也不会重传),而是因为被动关闭方重传了它的 FIN。事实上,被动关闭方总是重传 FIN 直到它收到一个最终的 ACK(怕网络延迟后者没有收到最后一个ACK,再重新发送时另外一方已经关闭.事实上是一直发送)

2. FIN_WAIT_2 + CLOSE_WAIT(用处 面试题)

半关闭(半连接,半开关)状态,就是先发起一段不能在发送东西给后发起端,但是后发起端可以发送东西给先发起端.

当TCP 链接中 A 向 B 发送 FIN 请求关闭,另一端 B 回应 ACK 之后(A 端进入 FIN_WAIT_2状态),并没有立即发送 FIN 给 A,A 方处于半连接状态(半开关),此时 A 可以接收 B 发送的数据,但是 A 已经不能再向 B 发送数据。

手动设置半关闭状态
#include <sys/socket.h> 
int shutdown(int sockfd, int how); 
sockfd: 需要关闭的socket的描述符
how: 允许为shutdown操作选择以下几种方式:
	SHUT_RD(0): 关闭sockfd上的读功能,此选项将不允许sockfd进行读操作。
        		 该套接字不再接收数据,任何当前在套接字接受缓冲区的数据将被无声的丢弃掉。 
    SHUT_WR(1): 关闭sockfd的写功能,此选项将不允许sockfd进行写操作。进程不能在对此套接字发 出写操作。
    SHUT_RDWR(2):关闭sockfd的读写功能。相当于调用shutdown两次:首先是以SHUT_RD,然后以 SHUT_WR。

使用 close 中止一个连接,但它只是减少描述符的引用计数,并不直接关闭连接,只有当描述符的引用计数为 0 时才关闭连接。shutdown 不考虑描述符的引用计数,直接关闭描述符。也可选择中止一个方向的连接,只中止读或只中止写。

注意:

  1. 如果有多个进程共享一个套接字,close 每被调用一次,计数减 1 ,直到计数为 0 时,也就是所用

​ 进程都调用了 close,套接字将被释放。

  1. 在多进程中如果一个进程调用了 shutdown(sfd, SHUT_RDWR) 后,其它的进程将无法进行通信。

​ 但如果一个进程 close(sfd) 将不会影响到其它进程

12. 端口复用

端口复用最常用的用途是:

  • 防止服务器重启时之前绑定的端口还未释放
  • 程序突然退出而系统没有释放端口
  • 端口复用一定要在绑定之前
#include <sys/types.h> 
#include <sys/socket.h> 





//返回值:成功0,失败-1
int getsockopt(int sockfd, int level, int optname, const void *optval, socklen_t optlen);
int setsockopt(int sockfd, int level, int optname, const void *optval, socklen_t optlen);
/*
参数
	- sockfd : 要操作的文件描述符
	- level : 级别
    	= SOL_SOCKET(端口复用的级别) 
	- optname: 选项的名称
		= SO_REUSERADDR		复用IP
		= SO_REUSERPORT		复用端口
	- optval : 端口复用的值(整形)
		= 1:可以复用
		= 0:不可以复用
	- optlen: optval大小
*/

13.查看网络相关数据

netstat
    参数:
	-a	所有的socket
    -p	显示正在使用的socket的程序名称
    -n  直接显示IP地址,不通过域名服务器 

*14. IO多路复用/IO多路转接

I/O 内存和文件的交互(读,写)

I/O 多路复用使得程序能同时监听多个文件描述符,能够提高程序的性能,Linux 下实现 I/O 多路复用的系统调用主要有select、poll 和 epoll

1. IO模型
BIO(阻塞IO模型)

因为 accept 和 read 都是阻塞的.必须等待一件事发生才能继续下一件事

解决:,需要接受多个时就必须进行多线程.

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wbFtnmIN-1646401901305)(第四章 通信编程.assets/image-20220304153148327.png)]

NIO(非阻塞IO模型)

因为不阻塞,所有每次循环的时候都会调用一次,这样就会造成资源浪费.

解决: select/poll/epoll 多路复用

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pocfxhMF-1646401901305)(第四章 通信编程.assets/image-20220304154211215.png)]

多路复用

就是委托 **内核帮程序检测(给文件描述符)**是否有信息过来,如果有信息过来再处理,而不是一直循环等待处理!

select : 只会告诉你有多少个信息需要处理,但没告诉你是哪一个需要处理.(需要自己遍历找到信息)

​ 本质就是内核检测到数据,在文件描述符一样大小的数组中把需要通信的文件描述符置1,然后让程序遍历

epoll : 它不知会告诉有多少个,也会告诉是哪几个!(需要自己调用函数FD_ISSET来判断)

2. select
1.步骤
  1. 首先要构造一个关于文件描述符的列表,将要监听的文件描述符添加到该列表中。

    然后内核逐个检测.有数据改变就1不变,如果没有就置0.再统一返回;

  2. 调用一个系统函数,监听该列表中的文件描述符,直到这些描述符中的一个或者多个进行I/O操作时,该函数才返回

    • 这个函数是阻塞,直到给与的文件描述符需要IO操作时,才返回
    • 函数对文件描述符的检测的操作是由内核完成
    • 在返回时,它会告诉进程有多少描述符要进行I/O操作(只说了数量)
2.实现函数
//sizeof(fd_set) = 128;  //128个字节,所以有1024位.最多检测1024个文件描述符,数组
#include <sys/time.h>
#include <sys/types.h> 
#include <unistd.h>
#include <sys/select.h> 


//设置select,把需要检测的设置位1,不需要的为0.为0的内核就不检测 
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
/*
参数:
	1.	nfds : 		委托内核检测的最大文件描述符的值 + 1;
	2.	readfds :	要检测的文件描述符的读的集合,把需要检测的文件描述符置1.传递给内核.
					本质就是内核检测有无数据需要读(接受)的;
					传入传出参数(传入传出均可用).
	3.	writefds :  与readfds差不多,但是是检测需要写(传输)的文件描述符
					本质就是检测里面是否有空闲,可以写;
	4.	exceptfds : 检测发生异常的文件描述符集合
	5.	timeout : 	设置的超时时间;*/
        struct timeval
        {
            long	tv_sec;		// 秒
            long 	tv_usec;	// 微妙
        }
		= NULL : 						永久阻塞
        = tv_sec + tv_usec = 0 : 		非阻塞
		= tv_sec + tv_usec > 0 :		阻塞对应的时间 
//返回值:  = -1: 失败  >0: n个文件描述符发生变化  =0 : 超时了还没有检测到

//情况fd_set,fd_set一共有1024 bit, 全部初始化为0.
void FD_ZERO(fd_set *set);
//将参数文件描述符fd对应的标志位设置为0;
void FD_CLR(int fd,fd_set *set);
// 将参数文件描述符fd 对应的标志位,设置为1 
void FD_SET(int fd, fd_set *set);
// 判断fd对应的标志位是0还是1, 返回值 :fd对应的标志位的值,0,返回0, 1,返回1 
int FD_ISSET(int fd, fd_set *set); 


/*细节
	1.检测当前是否有连接的文件描述符肯定是最小的,所有判断玩后循环在 文件描述符+1开始.到maxfd结束
	2.断开的时候需要记得,清除set 
*/		
3.缺点
  1. 每次调用select,都需要把fd集合从用户态拷贝到内核态,这个开学在fd很多时会很大
  2. 同时每次调用select都需要在内核遍历所有fd,这个fd很多时也很大
  3. select支持的文本描述符数量太小了,默认1024
  4. fd_set 集合不能复用,每次都需要重置
3. poll
1.定义

强化版select,把fd_set转化为结构体

优点

  1. 没有限制个数
  2. 可以复用

缺点

  1. 还是需要内核去遍历
2.函数
#include <poll.h> 
struct pollfd 
{ 
    int fd;				 /* 委托内核检测的文件描述符 */ 
    short events; 		 /* 委托内核检测文件描述符的什么事件 */(read/write)
    short revents;		 /* 文件描述符实际发生的事件 */ 
};

//初始化
struct pollfd myfd; 
myfd.fd = 5;
myfd.events = POLLIN | POLLOUT;

int poll(struct pollfd *fds, nfds_t nfds, int timeout);
/*
	fds  : 是一个struct pollfd 结构体数组,这是一个需要检测的文件描述符的集合;
	nfds : 这是第一个参数数组中最有一个有效元素的下标 + 1
    timeout : 阻塞时长
    	=0  不阻塞
    	-1	阻塞
    	>1 	阻塞时
    	
返回值: -1 失败 / >0 成功,有n个文件出现变化 
*/

/*细节
	判断返回的是read还是write,需要用位运算 &; 主要&上后等于1,说明就是&上后的功能
*/

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-aQ9cBFLY-1646401901306)(第四章 通信编程.assets/image-20220304201027668.png)]

15. !!epoll!!

1. 相关概念
#include <sys/epoll.h>


//获得epfd,创建实例.。在内核中创建了一个数据(结构体),这个数据中有两个比较重要的数据,一个是需要检测的文件描述符的信息(红黑树),还有一个是就绪列表,存放检测到数据发送改变的文件描述符信息(双向链表)。直接在内核创建(减少开销),epfd是文件描述符
int epfd = epoll_create(int size);
/*
参数:
	size	无意义,只要大于0均可,废弃
	
返回值: 失败: -1  / 成功: >0(操作epoll事例的文件描述符)
*/

/*
实例内有
{
	rb_root 黑红树		存放还没有检测的文件描述符数组
	rdlist 	双链表		已经检测到需要处理的文件描述符
}
*/

typedef union epoll_data {
    void *ptr; 			//参数
    int fd;				//文件描述符
    uint32_t u32; 
    uint64_t u64; 
} epoll_data_t;

//可以重用
struct epoll_event 
{
    //多个用或连接
    uint32_t events; /* 检测事件 */ 
        //场景epoll检测事件
            - EPOLLIN	检测输入
            - EPOLLOUT	检测输出
            - EPOLLERR	检测错误
            - EPOLLET	设置 模式为边缘模式
     

    epoll_data_t data; /* 用户数据信息 */ 
};	


//在eventpoll中添加或者删除待检测的文件描述符,ev为事件
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
//epoil_ctl(epfd,EPOLL_CTL_ADD,lfd,&ev) ;
/*
参数:
	-epfd : epoll对应的文件描述符
	-op : 要进行什么操作 
		=EPOLL_CTL_ADD: 添加 
		=EPOLL_CTL_MOD: 修改 
		=EPOLL_CTL_DEL: 删除 
	- fd : 要检测的文件描述符
    - event : 检测文件描述符什么事情
*/
//执行完回调,把rdlist中返回到服务端中,只是个指针,所有速度块


//检测函数
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
/*
参数:
	- epfd : epoll	实例对应的文件描述符
    - events : 传入传出参数,事件数组,传出:保存了发送了变化的文件描述符的信息.知道是哪个的,所有不需要自己遍历!!!!
    - maxevents : 第二个参数结构体数组的大小
    - timeout : 阻塞时间 
    	=0 : 不阻塞 
    	=-1 : 阻塞,直到检测到fd数据发生变化,解除阻塞 
    	>0 : 阻塞的时长(毫秒) 

返回值: - 成功,返回发送变化的文件描述符的个数 > 0.遍历数目
		- 失败 -1
*/


//注意
- 结束的时候.即收到len == 0;需要把结构体从数组中删掉(调用epoll_ctl);
- 最后需要关闭epfd!

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7IxqPQnJ-1646566700553)(第四章 通信编程.assets/image-20220306152358431.png)]

2.工作模式:
  1. LT模式(水平触发) - 通知多次,知道全部读完
    1. 读缓冲区有数据 - > epoll检测到了会给用户通知
      • 用户不读数据,数据一直在缓冲区,epoll 会一直通知
      • 用户只读了一部分数据,epoll会通知
      • 缓冲区的数据读完了,不通知

    ​ LT(level - triggered)是缺省的工作方式**(默认模式**),并且同时支持 blockno-block socket。在这种做法中,内核告诉你一个文件描述符是否就绪了,然后你可以对这个就绪的 fd 进行 IO 操作。如果你不作任何操作,内核还是会继续通知你的。(传出数组events一定会传出)

  2. ET模式(边缘触发) - 通知一次,爱管不管
    1. 读缓冲区有数据 - > epoll检测到了会给用户通知

      • 用户不读数据,数据一直在缓冲区,epoll 下次检测的时候不会不通知

      • 用户只读了一部分数据,epoll不通知

      • 缓冲区的数据读完了,epoll不通知

    2. 一般都是搭配非阻塞函数和循环读数据

    ​ ET(edge - triggered)是高速工作方式,只支持 no-block socket。在这种模式下,当描述符从未就绪变为就绪时,内核通过epoll告诉你。然后它会假设你知道文件描述符已经就绪,并且不会再为那个文件描述符发送更多的就绪通知,直到你做了某些操作导致那个文件描述符不再为就绪状态了。但是请注意,如果一直不对这个 fd 作 IO 操作(从而导致它再次变成未就绪),内核不会发送更多的通知(only once)。 只通知一次,默认通知你就知道了.就算你不读取,

    ​ ET 模式在很大程度上减少了 epoll 事件被重复触发的次数,因此效率要比 LT 模式高。epoll工作在 ET 模式的时候,必须使用非阻塞套接口(read,write等!),以避免由于一个文件句柄的阻塞读/阻塞写操作把处理多个文件描述符的任务饿死。

    • 本质就算一次返回就只epoll_wait一次!,所有需要循环读取
    • 而且消息是个队列,先进先出.如果你前面没读完.后面读取的时候会优先读取前一次的数据

16 .面试

1. select/poll工作流程
  1. fds从用户空间拷贝到内核中
  2. 内核去遍历数组,确定哪些fd需要处理
  3. 遍历完从内核拷贝到用户
2. epoll工作流程
  1. 在内核指定区域申请一份空间(create)
  2. 通过用户的操作可以把需要处理的fd压入这片空间中(底层是个红黑树) (ctl)
  3. 内核遍历压入的fd,有需要处理的就拷贝一份放到一个双向链表中(wait).
  4. 返回双向链表的fd到传入的数组中
3,什么情况下select性能会比epoll好?

在IO数量不多,但是多线程操作的时候,因为红黑树免不了要加锁.

17. TCP拥塞控制 (Tahoe,Reno,BBR)
  1. 慢启动 :当 cwnd < ssthresh,使用慢启动.cwnd每收到一次确认都会使自己的窗口大小加倍
  2. 拥塞避免: 当cwnd>=ssthresh时,就会启动拥塞避免.cwnd编程线性增长(每收到一个确认+1)
  3. 超时: 将ssthresh设置为原来cwnd的一半(ssthresh = cwnd / 2),cwnd重置为1(cwnd = 1),开始慢启动
  4. 快重传
    1. Tahoe算法下如果收到三次重复确认,就进入快重传立即重发丢失的数据包,同时将慢启动阈值设置为当前拥塞窗口的一半,将拥塞窗口设置为1MSS,进入慢启动状态;
    2. Reno算法如果收到三次重复确认,就进入快重传,但不进入慢启动状态,而是直接将拥塞窗口减半,进入拥塞控制阶段,这称为“快恢复”
18. TCP 可靠性保证
  1. 检验和
  2. 序列号/确认应答
  3. 超时重传
  4. 最大消息长度
  5. 滑动窗口控制(流量控制)
  6. 拥塞控制
19. 滑动窗口(流量控制)

​ 滑动窗口协议是传输层进行流控的一种措施,接收方通过通告发送方自己的窗口大小,从而控制发送方的发送速度,从而达到防止发送方发送速度过快而导致自己被淹没的目的.TCP的滑动窗口解决了端到端的流量控制问题,允许接受方对传输进行限制,直到它拥有足够的缓冲空间来容纳更多的数据。

20.重传机制
  1. TCP在发送数据时会设置一个计时器,若到计时器超时仍未收到数据确认信息,则会引发相应的超时或基于计时器的重传操作,计时器超时称为重传超时(RTO) 。另一种方式的重传称为快速重传,通常发生在没有延时的情况下。若TCP累积确认无法返回新的ACK,或者当ACK包含的选择确认信息(SACK)表明出现失序报文时,快速重传会推断出现丢包,需要重传。
21.流量控制和拥塞控制的区别

流量控制解决的是发送方和接收方速率不匹配的问题,发送方发送过快接收方就来不及接收和处理。采用的机制是滑动窗口的机制,控制的是发送了但未被Ack的包数量。端到端的

拥塞控制解决的是避免网络资源被耗尽的问题,通过大家自律的采取避让的措施,来避免网络有限资源被耗尽。当出现丢包时,控制发送的速率达到降低网络负载的目的。 A与B之间的网络发生堵塞导致传输过慢或者丢包,来不及传输。

22.说说如果三次握手时候每次握手信息对方没收到会怎么样,分情况介绍
  1. 如果第一次握手消息丢失,那么请求方不会得到ack消息,超时后进行重传

  2. 如果第二次握手消息丢失,那么请求方不会得到ack消息,超时后进行重传

  3. 如果第三次握手消息丢失,那么Server 端该TCP连接的状态为SYN_RECV,并且会根据 TCP的超时重传机制,**会等待3秒、6秒、12秒后重新发送SYN+ACK包,以便Client重新发送ACK包。**而Server重发SYN+ACK包的次数,可以设置/proc/sys/net/ipv4/tcp_synack_retries修改,默认值为5.如果重发指定次数之后,仍然未收到 client 的ACK应答,那么一段时间后,Server自动关闭这个连接。

    client 一般是通过 connect() 函数来连接服务器的,而connect()是在 TCP的三次握手的第二次握手完成后就成功返回值。也就是说 client 在接收到 SYN+ACK包,它的TCP连接状态就为 established (已连接),表示该连接已经建立。那么如果 第三次握手中的ACK包丢失的情况下,Client 向 server端发送数据,Server端将以 RST包响应,方能感知到Server的错误.

23.简述 TCP 的 TIME_WAIT,为什么需要有这个状态
  1. TIME_WAIT状态也称为2MSL等待状态。当TCP执行一个主动关闭,并发回最后一个ACK,该连接必须在TIME_WAIT状态停留的时间为2倍的MSL。这样可让TCP再次发送最后的ACK以防这个ACK丢失(另一端超时并重发最后的FIN)
  2. 这种2MSL等待的另一个结果是这个TCP连接在2MSL等待期间,定义这个连接的插口(客户的IP地址和端口号,服务器的IP地址和端口号)不能再被使用。这个连接只能在2MSL结束后才能再被使用。(可以做端口复用.)
  3. 理论上,原因:四个报文都发送完毕,就可以直接进入CLOSE状态了,但是可能网络是不可靠的,有可能最后一个ACK丢失。所以TIME_WAIT状态就是用来重发可能丢失的ACK报文。

课程:https://www.nowcoder.com/study/live/504.如果侵权请及时通知

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

公仔面i

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值