0%

计算机网络-应用层

Krakatoa_ZH-CN8471800710_1920x1080.jpg

计算机网络视频
计算机网络-基础知识
计算机网络-应用层
计算机网络-传输层
计算机网络-网络层
计算机网络-数据链路层

应用层是提供不同主机之间进程和进程的通信,应用层的数据单位是报文。常见的应用层协议包括:

  • Web:HTTP、HTTPS
  • Email:SMTP、POP3、IMAP
  • File:FTP

网络应用的体系结构

  • C/S:客户端-服务器
  • P2P:没有明确客户端和服务器的角色
  • 混合结构:比如文件传输使用 P2P 结构,文件搜索使用 C/S 结构

Web 应用

HTTP

多进程多线程浏览器

HTTP 协议是一个无状态的协议,如果设计成有状态的协议,维护状态需要进行非常复杂的处理。HTTP 默认是持久性连接。

  • 非持久性连接:一个 TCP 连接只能处理一个 HTTP 请求
  • 持久性连接:一个 TCP 连接能够处理多个 HTTP 请求

HTTP 包括请求消息和响应消息,首部包括通用首部字段、请求首部字段、响应首部字段和实体首部字段。

  • host:主机号在缓存或代理中使用

DNS 应用

域名解析系统 DNS 是将域名解析为 IP 地址的分布式分层数据库。DNS 提供以下几种服务:

  • 将域名映射成 IP 地址
  • 主机别名
  • 邮件服务器别名
  • 负载均衡:在进行域名向 IP 地址的映射时,可以提供多个 IP 地址

分布式

域名并不完全存储于一个服务器中,每个 DNS 服务器只保留它自己的数据。

分层

pic

域名具有层次结构,从上到下依次是:根域名、顶级域名、二级域名…

  • 根域名服务器:全球有 13 个根域名服务器
  • 顶级域名服务器:负责 educomorg 等顶级域名
  • 权威域名服务器:组织的域名解析服务器,负责组织内部的域名解析
  • 本地域名服务器:每个 ISP 都有一个本地域名服务器。本地域名服务器是默认的域名解析服务器,当主机进行 DNS 查询时,查询被发送到本地域名服务器,本地域名服务器作为代理将查询转发给域名解析系统。

迭代查询 & 递归查询

域名查询分为两种:迭代查询和递归查询

若客户端想要查询 dzapathy.github.io 的 IP,迭代查询的流程如下:

  • 首先主机将查询发送给本地域名服务器

  • 本地域名服务器无法解析域名时,本地域名服务器作为代理转发查询访问根域名服务器

  • 根域名服务器无法解析域名时,返回顶级域名服务器 io

  • 本地域名服务器查询顶级域名解析服务器 io

  • 顶级域名解析服务器无法解析域名时,返回权威域名服务器 github.io

  • 本地域名服务器查询权威域名服务器 github.io

  • 权威域名服务器 github.io 返回 dzapathy.github.io 的 IP 地址

  • 本地域名服务器将 dzapathy.github.io 的 IP 地址返回给客户端

若客户端想要查询 dzapathy.github.io 的 IP,递归查询的流程如下:

  • 首先主机将查询发送给本地域名服务器

  • 本地域名服务器无法解析域名时,本地域名服务器作为代理转发查询访问根域名服务器

  • 根域名服务器无法解析域名时,查询顶级域名服务器 io

  • 顶级域名服务器无法解析域名时,查询权威域名服务器 github.io

  • 权威域名服务器 github.io 返回 dzapathy.github.io 的 IP 地址给顶级域名服务器

  • 顶级域名服务器返回给根域名服务器

  • 根域名服务器返回给本地域名服务器

  • 本地域名服务器将 dzapathy.github.io 的 IP 地址返回给客户端

查询区别

  • 迭代查询本地服务器会发送多次查询请求,递归查询本地服务器会发送一次查询请求
  • 递归查询域名解析系统需要维护并转发查询请求,而迭代查询只需要响应一次查询请求
  • 一般主机和本地服务器之间使用递归查询,本地服务器和域名解析系统之间使用迭代查找

缓存

本地域名服务器一般会缓存顶级域名服务器的映射

DNS 记录

格式:$(name, value,type,ttl)$,$ttl$ 表示时间有效性

type = A 时,name 表示主机域名,value 表示 IP 地址

type = NS 时,name 表示域(比如edu.cn),value 表示权威域名服务器的主机域名

type = CNAME 时,name 表示真实域名的别名,value 表示真实域名

type = MS 时,value 是与 name 相对应的邮件服务器

DNS 底层协议

DNS 可以使用 UDP 或 TCP 进行传输,使用的端口号都为 53。大多数情况下 DNS 使用 UDP 进行传输,这就要求域名解析器和域名服务器都必须自己处理超时和重传从而保证可靠性。在两种情况下会使用 TCP 进行传输:

  • 如果返回的响应超过的 512 字节(UDP 最大只支持 512 字节的数据)
  • 区域传送(区域传送是主域名服务器向辅助域名服务器传送变化的那部分数据)

Email 应用

Email 应用包括邮件客户端、邮件服务器和邮件协议。邮件协议包含发送协议和读取协议,发送协议通常使用 SMTP 协议,读取协议通常使用 POP3 或 IMAP 协议。

  • 邮件客户端:发送、接收邮件
  • 邮件服务器:为每一个用户分配一个邮箱,并使用一个消息队列存储等待发送的邮件
  • 邮件协议:邮件服务器之间传递消息所使用的协议

发送协议

SMTP 协议是发送协议,依赖于 TCP 协议,使用命令 / 响应模式进行交互,SMTP 占用的端口号是 25 。

  • 持久性连接:只建立一次 TCP 请求
  • 消息必须使用 7 位 ASCII 码
  • SMTP 使用回车换行确认消息结束

消息格式

SMTP

pic

  • header:To、From、Subject

  • body:消息本身,只能是 ASCII

MIME

为了进行多媒体邮件扩展,在邮件头部增加额外的行声明 MIME 的内容类型,来支持发送多媒体。MIME 可以发送二进制文件,MIME 并没有改动或者取代 SMTP,而是增加邮件主体的结构,定义了非 ASCII 码的编码规则

pic

读取协议

邮件读取协议又称邮件访问协议,主要有 POP3 和 IMAP 协议。

  • POP3:提供认证/授权和下载功能,无状态协议
  • IMAP:更复杂,更多功能,能够操纵邮件服务器上的消息
  • HTTP:网页邮件

POP3

  • 认证过程:客户端发送用户名和密码,服务端响应
  • 事务阶段:
    • List:列出消息数量
    • Retr:用编号获取消息
    • Dete:删除邮件
    • Quit:退出

POP3 模式:

  • 下载并删除:下载后在服务器删除

  • 下载并保持:下载后服务器也会继续保存,支持不同客户端多次浏览

IMAP

  • 所有消息保存在服务器上
  • 允许用户利用文件夹组织信息
  • IMAP 是有状态协议

P2P 应用

P2P 没有明确的客户端和服务器的角色,而是点对点的应用,P2P 采用共享的机制分发文件。

危害

  • 占用网络带宽
  • 助长病毒传播
  • 版权问题

索引

  • 集中式索引

    节点加入时将 IP 和持有的内容通知给中央服务器,中央服务器去维护所有节点的索引信息。集中索引存在单点故障、性能瓶颈、版权等问题

  • 洪泛式查询

    完全分布式的架构,每个节点对它共享的文件进行索引且只对它共享的文件进行索引。查询时向已有的 TCP 连接发送查询请求,任何收到查询请求的节点会转发查询请求。如果查询命中,利用反向路径发回查询节点

  • 层次式覆盖网络

    层次式覆盖网络是介于集中式索引和洪泛式查询之间的方法。网络节点分为两类:普通节点和超级节点。普通节点只和超级节点建立 TCP 连接,这部分可以看成集中式索引;超级节点维护索引信息,超级节点之间建立 TCP 连接,这部分可以看成洪泛式查询

    pic

FTP

FTP

DHCP

DHCP

一个主机如何获取 IP 地址:

  • 静态配置: IP 地址、子网、默认网关和本地域名服务器 DNS
  • 动态配置:动态主机配置协议 DHCP,从 DHCP 服务器获取 IP 地址。DHCP 能够保证主机即插即用,并且允许地址重用

过程:

  • 主机发送”DHCP discover”发现报文,源 IP 地址和端口号是 $0.0.0.0:68$,目的 IP 地址和端口号是 $255.255.255.255:67$。广播的原因是为了发现子网内的 DHCP 服务器
  • DHCP 服务器利用”DHCP offer“ 提供报文进行响应,DHCP 进行广播的原因是因为此时主机还没有 IP 地址
  • 主机发送 ”DHCP request“ 请求报文,继续广播,原因是可能有多个 IP 地址,主机携带接受的 IP ,其他 DHCP 服务器可以为其他主机分配 IP
  • DHCP 服务器发送”DHCP ACK“ 确认报文,DHCP 进行广播的原因是因为此时主机还没有 IP 地址,最后主机动态分配了一个 IP 地址

pic

不过客户获取的IP一般是用租期,到期前需要更新租期

  • 当租期使用 50% ,客户端直接向为其提供 IP 的DHCP 服务器发送 DHCP request 报文
  • 如果收到 DHCP 服务器 ACK 报文更新租期;如果没有收到 ACK 报文继续使用
  • 当租期使用 87.5% ,再次向为其提供 IP 的DHCP 服务器发送 DHCP request 报文
  • 如果收到 DHCP 服务器 ACK 报文更新租期;如果没有收到 ACK 报文发送 DHCP offer 重新获取 IP

TELNET

TELNET 用于登录到远程主机上,并且远程主机上的输出也会返回。TELNET 可以适应许多计算机和操作系统的差异,例如不同操作系统系统的换行符定义

网络编程

Socket API 对外使用 IP 地址和端口号来区分 Socket,对内操作系统使用套接字描述符来区分 Socket 。每个进程有一个端口号,可以有多个 Socket 。每个进程维护一个 Socket 描述符表,存储套接字描述符,套接字描述符指向对应的 Socket 数据结构。Socket 数据结构中包含源 IP 地址,源端口号,目的 IP 地址和目的端口号,以此来区分唯一 Socket 连接

pic

端口号的范围是$0 \sim 65535$,其中 $0 \sim 1023$ 是知名端口号, $1024 \sim 65535$ 动态端口号。知名端口号为特定的网络应用提供服务端口

pic

处理单个连接请求

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
package com.apathy;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;

public class SocketServer {

// 单个连接的demo
public static void main(String[] args) throws IOException {
ServerSocket serverSocket = new ServerSocket(9000); // 创建 Socket
Socket socket = serverSocket.accept(); // 监听客户端连接

// 创建输入输出流
DataInputStream dis = new DataInputStream(
new BufferedInputStream(socket.getInputStream()));
DataOutputStream dos = new DataOutputStream(
new BufferedOutputStream(socket.getOutputStream()));

while(true) {
double length = dis.readDouble();
if(length < 0)
break;
System.out.println("服务器端收到的正方形边为:" + length);
double result = length * length;
dos.writeDouble(result);
dos.flush();
}
socket.close();
serverSocket.close();
}
}
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
package com.apathy;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.net.Socket;

public class SocketClient {

public static void main(String[] args) throws IOException {
Socket socket = new Socket("localhost", 9000);

DataInputStream dis = new DataInputStream(
new BufferedInputStream(socket.getInputStream()));
DataOutputStream dos = new DataOutputStream(
new BufferedOutputStream(socket.getOutputStream()));

// 使用 for 循环模拟多次计算
for(int i = 5; i >= 0; i--) {
System.out.println("请输入正方形边长(小于0退出)");
double length = i - 0.5;
dos.writeDouble(length);
dos.flush();
if(length < 0)
break;
double res = dis.readDouble();
System.out.println("计算结果为:" + res);
}
socket.close();
}
}

处理多个连接请求

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
63
64
65
66
67
68
69
70
71
72
73
package com.apathy;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicInteger;

public class SocketServer {
public static void main(String[] args) throws IOException {
ServerSocket serverSocket = new ServerSocket(8888);
AtomicInteger number = new AtomicInteger();

ExecutorService exec = Executors.newCachedThreadPool();

try {
while (true) {
Socket socket = serverSocket.accept();
exec.execute(new SimpleServer(socket, number.getAndIncrement()));
}
} finally {
serverSocket.close();
}
}
}

class SimpleServer implements Runnable {

private Socket socket;
private int num;

public SimpleServer(Socket socket, int num) {
this.socket = socket;
this.num = num;
}

@Override
public void run() {
DataInputStream dis = null;
DataOutputStream dos = null;
try {
System.out.println("客户端 " + num + "建立连接");
dis = new DataInputStream(new BufferedInputStream(socket.getInputStream()));
dos = new DataOutputStream(new BufferedOutputStream(socket.getOutputStream()));
while(true) {
double length = dis.readDouble();
if(length < 0)
break;
System.out.println("收到客户端 " + num+ " 的正方形边为:" + length);
double result = length * length;
dos.writeDouble(result);
dos.flush();
}
} catch(IOException e) {
e.printStackTrace();
} finally {
try {
if(dis != null)
dis.close();
if(dos != null)
dos.close();
socket.close();
}catch(IOException e) {
e.printStackTrace();
}
}
}
}

异步 IO 处理多个请求

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
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
// 服务端
package com.apathy;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
import java.util.Set;

public class SocketNIOService {

public static void main(String[] args) throws IOException{
Selector sel = Selector.open(); // 创建选择器

// 创建ServerSocketChannel,监听新进来的TCP连接
ServerSocketChannel socketChannel = ServerSocketChannel.open();
socketChannel.configureBlocking(false); // 设置非阻塞 IO
/* 将通道注册到选择器上
SelectionKey.OP_CONNECT:连接就绪,一个Channel成功连接到另一个服务器
SelectionKey.OP_ACCEPT:接收就绪,一个ServerSocketChannel准备好接收新进入的连接
SelectionKey.OP_READ:读就绪,一个有数据可读的通道
SelectionKey.OP_WRITE:写就绪,一个等待写数据的通道
*/
socketChannel.register(sel, SelectionKey.OP_ACCEPT);

// 将 ServerSocketChannel 绑定到特定端口
ServerSocket serverSocket = socketChannel.socket(); // 创建套接字
InetSocketAddress address = new InetSocketAddress("127.0.0.1", 8888);
serverSocket.bind(address); // 绑到特定端口

while(true) {
sel.select(); // 监听事件,没有事件会阻塞
Set<SelectionKey> set = sel.selectedKeys(); // 获取到达的事件
Iterator<SelectionKey> iterator = set.iterator(); // 使用迭代器遍历
while(iterator.hasNext()) { // 轮询
SelectionKey key = iterator.next();
if(key.isAcceptable()) { // 连接就绪
// 如果是连接请求,将连接通道注册到 selector 上
ServerSocketChannel ssc = (ServerSocketChannel)key.channel();
SocketChannel sc = ssc.accept();
sc.configureBlocking(false);
sc.register(sel, SelectionKey.OP_READ);
} else if(key.isReadable()) { // 读就绪
SocketChannel sc = (SocketChannel) key.channel();
System.out.println(readDataFromSocketChannel(sc));
sc.close();
}
iterator.remove();
}
}
}

private static String readDataFromSocketChannel(SocketChannel sChannel) throws IOException {

ByteBuffer buffer = ByteBuffer.allocate(1024);
StringBuilder data = new StringBuilder();

while (true) {
buffer.clear();
int n = sChannel.read(buffer);
if (n == -1) {
break;
}
buffer.flip();
int limit = buffer.limit();
char[] dst = new char[limit];
for (int i = 0; i < limit; i++) {
dst[i] = (char) buffer.get(i);
}
data.append(dst);
buffer.clear();
}
return data.toString();
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 客户端
package com.apathy;

import java.io.IOException;
import java.io.OutputStream;
import java.net.Socket;

public class SocketNIOClient {

public static void main(String[] args) throws IOException{
Socket socket = new Socket("127.0.0.1", 8888);
OutputStream ops = socket.getOutputStream();
String str = "hello, apathy";
ops.write(str.getBytes());
ops.close();
socket.close();
}
}