0%

黑马C++网络编程

OSI七层模型-TCP/IP模型

image-20210610212135741

各个层级协议

层级 协议
应用层 http、ftp、nfs、ssh、telnet
传输层 tcp、udp
网络层 IP、ICMP、IGMP
链路层 以太网帧协议、ARP

C/S、B/S模型优缺点

C/S B/S
优点 缓存大量数据、协议选择灵活、速度快 安全性较高、跨平台、开发工作量小
缺点 开发工作量较大、安全性、不能跨平台 不能缓存大量数据、阉割遵守http协议

网络传输流程

数据没有封装之前,是不能在网络中传递

数据-应用层-传输层-网络层-链路层 网络环境

协议简介

  • 以太网帧协议

    • ARP:根据IP地址获取mac地址
    • 以太网帧协议:根据MAC地址,完成数据包传输
  • IP协议

    • 版本:IPv4、IPv6

    • TTL

      • time to live
      • 设置数据包在路由节点中的跳转上限。每经过一个路由 节点,该值-1,减为0的路由有义务将该数据包丢弃
    • 源IP:32位 — 4字节 192.168.1.108—点分十进制

    • 目的IP:32位—-4字节

      IP地址可以在网络环境中唯一标识一台主机

      端口号:可以在网络的一台主机上,唯一的标识一个进程

      IP地址+端口号:唯一标识一个进程

  • UDP

    • 16位源端口号
    • 16位目的端口号
  • TCP

    • 16位源端口号
    • 16位目的端口号
    • 32位序号
    • 32确认序号
    • 6个标志位
    • 16位窗口大小 2^16=65536

网络套接字

  • 小端法(本地字节序)

    • 高位高地址、低位低地址

    • int a = 0x12345678

      • image-20210610213342655
  • 大端法(网络字节序)

    • 高位低地址、低位高地址
  • htonl –> 本地 –> 网络(IP)

  • htons –> 本地 –> 网络(port)

  • ntohl –> 网络(IP) –> 本地

  • ntohs –> 网络(port) –> 本地

IP地址转换

  • int inet_pton(int af, const char *src, void *dst);

    • 本地字节序转换为网络字节序
    • af:AF_INET、AF_INET6
    • src:IP地址(点分十进制)
    • dst:传出:转换后的网络字节序 IP地址
    • 返回值:
      • 成功返回 1
      • 异常返回 0,说明src指向的不是一个有效的ip地址
      • 失败返回 -1
  • const char *inet_ntop(int af, const void *src,char *dst, socklen_t size);

    • 本地字节序转换为网络字节序

    • af:AF_INET、AF_INET6

    • src:网络字节序IP地址

    • dst:本地字节序

    • size:dst的大小

    • 返回值:

      • 成功返回 dst
      • 失败返回 NULL

sockaddr地址结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
struct sockaddr_in addr

addr.sin_family = AF_INET;

addr.sin_port = htons(8080);

int dst;

inet_pton(AF_INET,"127.0.0.1",(void *)&dst);

addr.sin_addr.s_addr = dst;

addr.sin_addr.s_addr = htonl(INADDR_ANY);

bind(fd,(struct sockaddr*)&addr,size);

socket模型穿件流程图

image-20210610213819187

一个服务端和一个客户端建立通信一共三个套接字

服务端、客户端

  • 服务端:

    • bind()绑定ip+port
- listen()设置同时监听上限



- accept()阻塞监听客户端连接
  • 客户端:
    • connect()绑定ip+port

套接字函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
#include <sys/socket.h>

int socket(int domain, int type, int protocol);
/**
* 创建一个套接字
* domain: AF_INET、AF_INET6、AF_UNIX
* type:SOCK_STREAM、SOCK_DGRAM
* protocol:0
* 返回值:
* 成功:新套接字所对应的文件描述符
* 失败:-1 errno
**/

int bind(int sockfd, const struct sockaddr *addr,socklen_t addrlen);
/**
* 给socket绑定一个地址结构
* sockfd:socket函数返回值
* struct sockaddr_in addr;
* addr.sin_family = AF_INET;
* addr.sin_port = htons(8888);
* addr.sin_addr.s_addr = htonl(INADDR_ANY);
* addr:传入参数(struct sockaddr*)&address
* addrlen:sizeof(addr) 地址结构的大小
* 返回值:
* 成功:0
* 失败:-1 error
**/

int listen(int sockfd, int backlog);
/**
* 设置同时与服务器建立连接的上线数(同时进行3次握手 的客服端数量)
* sockfd:socket函数返回值
* backlog:上限的值
* 返回值:
* 成功:0
* 失败: -1 error
**/

int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
/**
* 阻塞等待客服端建立连接,成功返回一个与客户端成功连接的socket文件描述符
* sockfd:socket函数返回值
* addr:传出参数。成功与服务器建立连接的那个客服端的地址结构
* addrlen:&client_addr_len。传入传出。入:addr的大小,出:客服端addr的实际大小
* socket_t client_addr_len = sizeof(addr);
* 返回值:
* 成功:能与服务器进行数据通信的socket对应的文件描述符
* 失败:-1 error
**/

int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
/**
* 使用现有socket与服务器建立连接
* sockfd:socket函数返回值
* addr:传入参数。服务器的地址结构(struct sockaddr*)&address
* addrlen:sizeof(addr) 服务器地址结构的大小
* 返回值:
* 成功:0
* 失败: -1 error
* 如果不使用bind()函数绑定客户端地质结构,采用"隐式绑定"。
**/

TCP通信流程分析

server client
socket() 创建socket socket() 创建socket
bind() 绑定服务器地址结构 connect() 与服务器建立连接
listen() 设置监听上限 write() 写数据到socket
accept() 阻塞监听客户端连接 read() 读数据
read() 读socket获取客户端数据 close()
write() 写数据
close() 关闭套接字

TCP协议

  • 三次握手

    • 主动发起连接请求端,发送 SYN 标志位,请求建立连接。携带数据包包号、数据字节数(0)、滑动窗口大小。
    • 被动接受连接请求端,发送 ACK 标志位,同时携带 SYN 请求标志位。携带序号、确认序号、数据字节数(0)、滑动窗口大小。
    • 主动发起连接请求端,发送 ACK 标志位,应答服务器连接请求。携带序号、确认序号
  • image-20210613142506250

  • 四次挥手

    • 主动关闭连接请求端,发送 FIN 标志位。
    • 被动关闭连接请求端,应答 ACK 标志位。 —–半关闭完成
    • 被动关闭连接请求端,发送 FIN 标志位。
    • 主动关闭连接请求端,应答 ACK 标志位 。——连接全部关闭

image-20210613152316871

  • 滑动窗口
    • 发送给连接对端,本段缓冲区实时大小,保证数据不会丢失

TCP状态时序图

image-20210629165910770

  1. 主动发起连接请求端:CLOSE — 发送SYN — SYN_SEND — 接收ACK、SYN — SYN_SEND — 发送方ACK — ESTABLISHED(数据通信状态)

  2. 主动关闭连接请求端:ESTABLISHED(数据通信状态) — 发送FIN — FIN_WAIT_1— 接收ACK — FIN_WAIT_2 — 接收对端发送的FIN — FIN_WAIT_2 — 回发ACK — TIME_WAIT(只有主动关闭连接方,会经历该状态) — 等2MSL时长 — CLOSED

  3. 被动接收连接请求端:CLOSE — LISTEN — 接收SYN — LISTEN — 发送ACK、SYN — SYN_RCVD — 接收ACK — ESTABLISHED

  4. 被动关闭连接请求端:ESTABLISHED—接收对端FIN — ESTABLISHED — 发送ACK — CLOSE_WAIT(说明对端(主动关闭连接端)处于半关闭状态) — 发送FIN — LAST_ACK — 接收ACK — CLOSE

重点记忆: ESTABLISHED、FIN_WAIT_2 、CLOSE_WAIT、TIME_WAIT(2MSL时长)

2MSL时长

一定出现在主动关闭连接请求一端

保证,最后一个ACK能被成功接收。(等待期间,对端没有接收到ACK,对端会再次发送FIN请求)

端口复用

1
2
int opt = 1; //设置端口复用
setsockopt(lfd,SOL_SOCKET,SO_REUSEADDR,(void *)&opt,sizeof(opt));

半关闭

通信双方只有一端关闭通信。——FIN_WAIT_2(半关闭状态)

close(cfd);

1
2
3
4
5
int shutdown(itn fd, int how)
/** how: SHUT_RD 关闭读
* SHUT_WR 关闭写
* SHUT_RDWR 关闭读写
**/

区别:

  • shutdown在关闭多个文件描述符应用的文件时,采用全关闭的方法,close只关闭一个

错误处理函数

封装目的

  • 在server.c编程过程中突出裸机,将出错处理与逻辑分开,可以直接跳转man手册

wrap.c

  • 存放网络信相关常用 自定义函数
  • 命名方式:
    • 系统调用函数首字母大写 方便查看man手册。如:Listen()、Accept()
  • 函数功能:调用系统调用函数,处理出错场景
  • 在server.c 和 client.c 中调用 自定义函数
    • server.c 和 wrap.c 生成 server
    • client.c 和 wrap.c 生成 client

wrap.h

  • 存放 网络通信相关常用 自定义函数原神(声明)

高并发服务器

多进程并发服务器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
Socket();				//创建监听套接字 lfd
Bind(); //绑定地址结构
Listen(); //设置监听上限
while(1)
{
cfd = Accpet(); //接收客户端连接请求
pid = fork(); //

if(pid == 0) //子进程read()-->小写转大写-->write(cfd)
{
close(lfd); //关闭用于建立连接的套接字
read();
upper();
write();
}else if(pid > 0)
{
close(cfd); //关闭用于与客户端通信的套接字 cfd

contiue;
}
}
  • 子进程

    • close(lfd); //关闭用于建立连接的套接字
    • read();
    • upper();
    • write();
  • 父进程

    • 注册信号捕捉函数:SIGCHLD

    • 在回调函数中,完成子进程回收

      ​ while ( waitpid());

多线程并发服务器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Socket();				//创建监听套接字 lfd
Bind(); //绑定地址结构
Listen(); //设置监听上限
while(1)
{
cfd = Accpet(); //接收客户端连接请求
pthread_create(&tid, NULL,tfn,NULL);
pthread_detach(tid);
}

//子线程
void *tfn(void *arg)
{
close(lfd);
read(cfd);
upper();
write(cfd);
pthread_exit((void*)10);
}

read函数返回值

  1. 大于0 实际读到的字节数
  2. 等于0 已经读到结尾(对端已经关闭)【重点】
  3. -1 应该进一步判断errno的值
    1. errno= EAGAIN or EWOULDBLOCK:设置了非阻塞方式读。没有数据到达
    2. errno= EINTR 慢速系统调用被 中断
    3. errno= “其他情况” 异常。

select多路IO转接

  • 原理:借助内核,select来监听,客户端连接、数据通 信事件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
/**
* 清空一个文件描述符集合
* fd_set rset;
* FD_ZERO(&rset);
**/
void FD_ZERO(fd_set *set);

/**
* 将监听的文件描述符,添加到集合中
* FD_SET(3,&rset);FD_SET(4,&rset);FD_SET(5,&rset);
**/
void FD_SET(int fd,fd_set*set);

/**
* 将一个文件描述符从监听集合中删除
* FD_CLR(4,&rset);
**/
void FD_CLR(int fd,fd_set *set);


/**
* 判断一个文件描述符是否在监听集合中
* FD_ISSET(4,&rset);
* 返回值:在:1;不在:0;
**/
int FD_ISSET(int fd,fd_set *set);


/**
* 判断一个文件描述符是否在监听集合中
* nfds: 监听的所有文件描述符中,最大文件描述符+1
* readfds: 读 文件描述符监听集合 传入传出参数
* writefds: 写 文件描述符监听集合 传入传出参数 NULL
* exceptfds: 异常 文件描述符监听集合 传入传出参数 NULL
* timeout: >0: 设置监听超时时长
NULL: 阻塞监听
0: 非阻塞监听,轮询
* 返回值:
>0: 所有监听 集合中,满足对应事件的总数。
0: 没有满足监听条件的文件描述符
-1: errno
**/
int select(int nfds, fd_set *readfds, fd_set *writefds,fd_set *exceptfds, struct timeval *timeout);

思路分析:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
lfd = socket();  					//创建套接字
bind(); //绑定地址结构
listen(); //设置监听上限

fd_set rset,allset; //创建r监听集合
FD_ZERO(&allset); //将监听r集合清空
FD_SET(lfd,&allset); //将lfd添加至读集合中

while(1)
{
rset = allset; //保存监听集合
ret = select(lfd+1,&rset,NULL,NULL,NULL); //监听文件描述符集合对应事件
if(ret>0){ //有监听的描述符满足对应事件
if(FD_ISSET(lfd,&rset))
{
cfd = accept(); //建立连接,返回用于通信的文件描述符
FD_SET(cfd,&allset); //添加到监听通信描述符集合中
}
for(i=lfd+1;i<=最大文件描述符;i++)
{
FD_ISSET(i,&rset);
read();
小---大
write();
}
}
}

-------------THE END-------------

欢迎关注我的其它发布渠道