计算机网络:分层模型&&应用层

OSI

分层模型

image-20230827150702534

image-20230907123035154

每一层利用下层提供的服务为基础来为上层提供服务,最终服务于应用层的应用程序

应用层

网络应用的体系结构

  • 客户-服务器模式 (C/S:client/server)
    • 服务器先且一直运行,守护在某些端口上
    • 可拓展性,可靠性比较差
    • 客户数量达到一定阈值后会断崖式下降
  • 对等模式 (P2P:Peer To Peer)
    • 任意端系统之间可以通信,每个节点即使客户端又是服务器
    • 自拓展性
    • 迅雷,快播😳
  • 混合体 客户-服务器和对等体系结构

跨端系统进程通信

通过套接字(socket)来从网络发送和接受报文,应用程序开发者控制套接字在应用层的一切,对套接字在传输层的控制几乎为0

socket具体看这篇文章: https://segmentfault.com/a/1190000041413541

简单来说tcp socket就是操作系统维护的一个五元组**[源IP, 源端口, 目的IP, 目的端口, 类型(TCP or UDP)],udp socket就是一个三元组[目的IP, 目的端口, 类型(TCP or UDP)]**,返回的socket就是这个数组的下标

跨端系统进程通信需要的信息

  • 目标主机地址:IP地址
  • 所采用传输层协议:TCP or UDP
  • 目标主机中目标进程的标识符:端口号

套接字是应用程序进程和传输层协议直接的接口,使用哪种传输层协议,需要结合应用进程的要求

  • 可靠数据传输
  • 吞吐量
  • 定时
  • 安全性

因特网提供了UDP和TCP两种传输层协议

  • TCP:Transmission Control Protocol
    • 面向连接
    • 可靠数据传输服务
    • 拥塞控制机制
  • UDP:User Datagram Protocol
    • 无连接
    • 不可靠
    • 无拥塞控制

TCP安全是靠安全套接字层(secure sockets layer,SSL)来实现的,SSL在应用层实现

HTTP

http 超文本传输协议(hypertext transfer protocol) 使用tcp作为传输层协议,是一个无状态协议(不保存关于客户的任何信息) 默认80端口

http/1.0采用非持续连接,http/1.1默认采用持续连接

非持续,持续连接区别在于一个对象传输结束后,是否立即关闭tcp连接

流水线,一个请求对象还没到达时,再去请求另一个对象

非流水线 一个一个完整请求,请求-到达-请求-到达…………

URL基本格式:

protocol://user:password@www.someSchool.edu/someDept/pic.gif:port
协议 用户名 口令 主机名 路径名 端口

HTTP请求报文的通用格式

image-20230827121842745

post方法时才会使用请求实体体

HTTP响应报文的通用格式

image-20230827122100051

http是无状态的,为了识别用户,实现某写功能,采用了cookie与用户身份联系起来

cookie组件

  • 请求和响应报文的cookie首部行
  • 用户端系统的cookie文件,由浏览器管理
  • web后端数据库

用户第一次访问某个web站点时,web站点会生成一个唯一识别码,并把他当成索引在后端数据库产生一个表项,发送的响应报文会有Set-cookie的首部行

Set-cookie: xxxxxxxxxxx

浏览器接受到响应报文后,会在本地cookie文件里添加一行服务器的主机名和cookie值,随后每次对该web的请求都会带有cookie首部行

Cookie: xxxxxxxxxxx

web缓存器(代理服务器)

可以配置浏览器,使http请求首先经过web缓存器,如果web缓存器存有请求对象的副本,直接返回给浏览器,否则缓存器向原始服务器http请求,把响应对象存储到web缓存器,再通过web缓存器和浏览器原本的tcp连接把对象再返回给浏览器

二八定律,使缓存效果良好

条件get方法

为了避免web缓存器里的副本和原始服务器里的版本不一致,存在一种机制:条件get方法

请求报文使用get方法,包含If-Modified-Since:首部行,就是一个条件get请求报文

具体过程

web缓存器向原始服务器请求对象,服务器发送的响应报文里下面一句

Last-Modified: Wed, 9 Sep 2023 06:23:45

缓存器存储对象时会记下修改时间

一段时间用户再次请求该对象时,缓存器会向服务器发送条件get请求报文

Get /path/xxx.jpg HTTP/1.1
Host: www.grxer.com
If-Modified-Since: Wed, 9 Sep 2023 06:23:45

如果对象已经修改,在get实体体里返回200ok 实体体里包含该对象,否则返回304 Not Modified,告诉web缓存器可以直接使用该对象

HTTP/1.1 304 Not Modified
......

(empty entity body)

E-Mail

image-20230828230533757

三个重要组成部分

  • 用户代理 user agent
  • 邮件服务器 mail server
  • 简单邮件传输协议SMTP simple mail transfer protocol

SMTP(推协议)

使用TCP作为传输层协议 默认25号端口,连续连接

image-20230829110643721

邮件服务器,TCP连接建立后会,进行SMTP握手来确认收发方地址等等,之后发送报文

报文分为首部和主体,由一个空行隔开,报文必须为7位ascii编码

From:hello@xxx.com
To:world@xxx.com
....

主体内容

MIME

对于一些不能用ascii编码表示的数据采用了MIME (Multipurpose Internet Mail Extensions)多用途互联网邮件扩展类型

使用MIME需要包含一些首部行

MIME版本

MIME-Version: 1.0

内容类型

Content-Type: [type]/[subtype]; parameter

内容传输编码方式

Content-Transfer-Encoding: [mechanism]

mechanism的值可以指定为“7bit”,“8bit”,“binary”,“quoted-printable”,“base64”。

邮件访问协议(拉协议)

POP3 Post Office Protocol–Version 3,第三版邮件协议

IMAP Internet Mail Access Protocol ,因特网邮件访问协议

HTTP

DNS

Domain Name System 域名系统,主要功能是将主机名解析为ip地址,通常是由其他应用层协议所使用

DNS运行在UDP之上,默认使用53号端口

DNS采用分布式,分层数据库,大致分为三种类型的服务器

  • 根DNS服务器
  • 顶级域(Top-Level Domain,TLD)DNS服务器
  • 权威DNS服务器

image-20230829180249922

DNS查询有两种方式:递归迭代。DNS客户端设置使用的DNS服务器一般都是递归服务器,它负责全权处理客户端的DNS查询请求,直到返回最终结果。而DNS服务器之间一般采用迭代查询方式。

递归:DNS服务器接收到客户机请求,必须使用一个准确的查询结果回复客户机。如果DNS服务器本地没有存储查询DNS 信息,那么该服务器会询问其他服务器,并将返回的查询结果提交给客户机。

迭代:DNS服务器本地如果没有请求的DNS信息,会向客户机返回能够解析该查询请求的DNS服务器地址,客户机再向这台DNS 服务器提交请求,依次循环直到返回查询的结果

访问 www.ytu.edu.cn

DNS服务器里存储了资源记录(Resource Record,RR),是一个四元组

image-20230830170605836

DNS报文

image-20230830181152543

wireshark一个dns返回报文

image-20230902212408489

P2P

p2p文件分发协议 BitTorrent

视频流和内容分发网

流式视频占据ISP流量的大部分,由于http流存在缺陷,一般采用DASH(Dynmic Adaptive Streaming over Http):经HTTP的动态适应性流来传输

DASH

服务器:

  • 将视频文件分割成多个块
  • 每个块独立存储,编码于不同码率(8-10种)
  • 告示文件(manifest file): 提供不同块不同码率的URL

客户端:

  • 先获取告示文件
  • 周期性地测量服务器到客户端的带宽
  • 查询告示文件,在一个时刻请求一个块,HTTP头部指定字节范围
    • 如果带宽足够,选择最大码率的视频块
    • 会话中的不同时刻,可以切换请求不同的编码块 (取决于当时的可用带宽)

CDN

Content Distribution Networks 内容分发网络

为了解决高并发流媒体服务,通过cdn全网部署缓存节点,存储服务内容,就近为用户提供服务,提高用户体验

怎么让用户知道部署的cdn服务器在哪?

  1. 通过manifest file文件里写入cdn信息

  2. 或者cdn利用dns截获和重定向请求(大多数)

利用dns截获和重定向请求

  1. 用户的本地 DNS 服务器(LDNS)将该 DNS 请求中继到目标服务器的权威 DNS 服务器目标服务器的权威DNS服务器并不返回一个 IP 地址,而是向 LDNS 返回一个第三方 CDN 的主机名
  2. LDNS再发送对第三方CDN的主机名的DNS请求,第三方CDN专用DNS基础设施根据它的选择策略将最符合的CDN的IP地址给LDNS
  3. LDNS将该CDN转发给用户主机
  4. 主机建立于CDN的TCP连接,发送http get请求,如果使用dash,会先获取告示文件等等

套接字编程

UDP–python

UDPServer

from socket import *
serverPort=1234
'''
创建 Socket
地址簇: AF_INET (IPv4)
类型: SOCK_DGRAM (使用 UDP 传输控制协议)
'''
serverSocket=socket(AF_INET,SOCK_DGRAM)
#绑定地址(host,port)到套接字,任何向port发送的分组将导向该套接字
serverSocket.bind(('',serverPort))#host=''表示绑定到所有可用接口
while True:
#recvfrom接收UDP数据,需要指定接收数据的最大长度,返回值是(data,address)
#address是客户ip地址和端口号的元组(ipaddr,port)
message,clientAddr=serverSocket.recvfrom(2048)
modifiedMessage=message.decode().upper()
#sendto发送UDP数据,参数二address是形式为(ipaddr,port)的元组
serverSocket.sendto(modifiedMessage.encode(),clientAddr)

UDPClient

from socket import *
serverName="127.0.0.1"
serverPort=1234
#操作系统会为我们的socket分配一个端口号
clientSocket=socket(AF_INET,SOCK_DGRAM)
message=input('string-->STRING: ')
clientSocket.sendto(message.encode(),(serverName,serverPort))
modifiedMessage,serverAddr=clientSocket.recvfrom(2048)
print(modifiedMessage.decode())
clientSocket.close()

TCP–python

TCPServer

from socket import *
serverPort=1234
'''
创建 Socket
地址簇address family: AF_INET (IPv4)
类型: SOCK_STREAM (使用 TCP 传输控制协议)
'''
serverSocket=socket(AF_INET,SOCK_STREAM)
serverSocket.bind(('',serverPort))
#开始TCP监听,参数backlog指定在拒绝连接之前,操作系统可以挂起的最大连接数量,可以理解为最大排队数量
serverSocket.listen(1)
while True:
#阻塞的被动等待tcp连接,握手完成后建立TCP连接,返回新套接字
connectionSocket,addr=serverSocket.accept()
print(addr)
#接收TCP数据
message=connectionSocket.recv(1024)
modifiedMessage=message.decode().upper()
#发送 TCP 数据,已经tcp建立连接,不用显示指明ip和port
connectionSocket.send(modifiedMessage.encode())
connectionSocket.close()

TCPClient

from socket import *
serverName="127.0.0.1"
serverPort=1234
clientSocket=socket(AF_INET,SOCK_STREAM)
#三次握手建立tcp连接
clientSocket.connect((serverName,serverPort))
message=input('string-->STRING: ')
clientSocket.send(message.encode())
modifiedMessage,serverAddr=clientSocket.recvfrom(2048)
print(modifiedMessage.decode())
clientSocket.close()

UDP–linux C

UDPServer

#include <arpa/inet.h>
#include <netinet/in.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <unistd.h>
#include <ctype.h>
#define PORT 1234
#define BUFFER_SIZE 2048
#define MAXQUEUE 5
void stringLow2Upper(char *string) {
for (int i=0; i<strlen(string); i++) {
string[i] =toupper(string[i]);
}
}
int main() {
struct sockaddr_in serverSockaddr;
serverSockaddr.sin_family = AF_INET;
serverSockaddr.sin_addr.s_addr = htonl(INADDR_ANY);
serverSockaddr.sin_port = htons(PORT);
//创建udp socket
int serverSocket = socket(AF_INET, SOCK_DGRAM, 0);
//绑定socket到本地端口
if (-1 == bind(serverSocket, (struct sockaddr *)&serverSockaddr,
sizeof(serverSockaddr))) {
perror("bind");
exit(-1);
}
while (1) {
char buffer[BUFFER_SIZE];
memset(buffer, 0, BUFFER_SIZE);
struct sockaddr_in clientSockaddr;
socklen_t length = sizeof(clientSockaddr);
// ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
// struct sockaddr *src_addr, socklen_t *addrlen);
//从套接字读取数据,把发送方的ip:port等放入clientSockaddr
recvfrom(serverSocket, buffer, BUFFER_SIZE, 0,
(struct sockaddr *)&clientSockaddr, &length);
printf("connect%s:%d\n", inet_ntoa(clientSockaddr.sin_addr),
ntohs(clientSockaddr.sin_port));
// ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
// const struct sockaddr *dest_addr, socklen_t addrlen);
stringLow2Upper(buffer);
sendto(serverSocket, buffer, BUFFER_SIZE, 0, (struct sockaddr *)&clientSockaddr, length);
}
}

UDPClient

#include <arpa/inet.h>
#include <netinet/in.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <unistd.h>
#define PORT 1234
#define BUFFER_SIZE 2048
int main() {
struct sockaddr_in serverSockaddr = {
.sin_addr.s_addr = inet_addr("127.0.0.1"),
.sin_port = htons(PORT),
.sin_family = AF_INET,
};
//创建socket
int clientSocket = socket(AF_INET, SOCK_DGRAM, 0);
char sendBuffer[BUFFER_SIZE], recvBuffer[BUFFER_SIZE];
socklen_t length = sizeof(serverSockaddr);
memset(sendBuffer, 0, BUFFER_SIZE);
memset(recvBuffer, 0, BUFFER_SIZE);
fgets(sendBuffer, BUFFER_SIZE, stdin);
sendto(clientSocket, sendBuffer, BUFFER_SIZE, 0,
(struct sockaddr*)&serverSockaddr, length);
recvfrom(clientSocket, recvBuffer, BUFFER_SIZE, 0,
(struct sockaddr*)&serverSockaddr, &length);
printf("%s",recvBuffer);
}

TCP–linux C

TCPServer

#include <arpa/inet.h>
#include <ctype.h>
#include <netinet/in.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <unistd.h>
#define PORT 1234
#define BUFFER_SIZE 2048
#define MAXQUEUE 5
void stringLow2Upper(char *string) {
for (int i = 0; i < strlen(string); i++) {
string[i] = toupper(string[i]);
}
}
int main() {
/*
struct sockaddr_in {
   short int sin_family; Address family
unsigned short int sin_port; Port number
struct in_addr sin_addr; Internet address
unsigned char sin_zero[8]; Same size as struct sockaddr
}
  */
struct sockaddr_in serverSockaddr;
serverSockaddr.sin_family = AF_INET; //地址簇
/*
网络字节序通常是大端字节序,linux小端序
hton:host to network
uint16_t htons(uint16_t hostlong) 16位从主机字节顺序转换成网络字节顺序
uint32_t htonl(uint32_t hostlong) 32位同上
*/
serverSockaddr.sin_addr.s_addr = htonl(INADDR_ANY); // IP地址
serverSockaddr.sin_port = htons(PORT); //端口
int serverSocket = socket(AF_INET, SOCK_STREAM, 0);
//绑定socket
// int bind(int sockfd, const struct sockaddr * addr, socklen_t len)
if (-1 == bind(serverSocket, (struct sockaddr *)&serverSockaddr,
sizeof(serverSockaddr))) {
perror("bind");
exit(-1);
}
// TCP监听端口
if (listen(serverSocket, MAXQUEUE) == -1) {
perror("listen");
exit(-1);
}
while (1) {
char buffer[BUFFER_SIZE];
memset(buffer, 0, BUFFER_SIZE);
struct sockaddr_in clientSockaddr;
socklen_t length = sizeof(clientSockaddr);
// Int accept(int sockfd, struct sockaddr *restrict addr, socklen_t
// *restrict len)
//阻塞等待tcp连接,返回连接的socket
int connectionSocket =
accept(serverSocket, (struct sockaddr *)&clientSockaddr, &length);
if (connectionSocket < 0) {
perror("connect");
exit(-1);
}
printf("connect%s:%d\n", inet_ntoa(clientSockaddr.sin_addr),
ntohs(clientSockaddr.sin_port));
//向套接字接受数据
// int recv(int sockfd, const void *buf, size_t nbytes, int flags);
recv(connectionSocket, buffer, BUFFER_SIZE, 0);
puts(buffer);
stringLow2Upper(buffer);
//向套接字发送数据
// Int send(int sockfd, const void *buf, size_t nbytes, int flags);
send(connectionSocket, buffer, BUFFER_SIZE, 0);
close(connectionSocket);
}
}

TCPClient

#include <arpa/inet.h>
#include <netinet/in.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <unistd.h>
#define PORT 1234
#define BUFFER_SIZE 2048
int main() {
char sendBuffer[BUFFER_SIZE], recvBuffer[BUFFER_SIZE];
memset(sendBuffer, 0, BUFFER_SIZE);
memset(recvBuffer, 0, BUFFER_SIZE);
struct sockaddr_in serverSockaddr;
serverSockaddr.sin_family = AF_INET;
serverSockaddr.sin_addr.s_addr = inet_addr("127.0.0.1");
serverSockaddr.sin_port = htons(PORT);
//创建套接字
int clientSocket = socket(AF_INET, SOCK_STREAM, 0);
// int connect(int sockfd, const struct sockaddr *addr, socklen_t len);
//利用套接字和目标主机建立tcp连接
if (connect(clientSocket, (struct sockaddr *)&serverSockaddr,
sizeof(serverSockaddr)) < 0) {
perror("connect");
exit(-1);
}
puts("string-->STRING");
fgets(sendBuffer, BUFFER_SIZE, stdin);
//发送数据
send(clientSocket, sendBuffer, BUFFER_SIZE, 0);
//接受数据
recv(clientSocket, recvBuffer, BUFFER_SIZE, 0);
puts(recvBuffer);
close(clientSocket);
}