1. 《网络是怎样连接的》笔记
1.1 探索浏览器内部
1.1.1 生成HTTP请求
- 与Web服务器的交互流程
- 用户在浏览器中输入URL(统一资源定位符,即网址)
- 浏览器解析URL,根据网址含义生成请求信息,并告知服务器
- 浏览器向DNS服务器查询域名对应的IP地址
- 全球DNS服务器接力完成IP地址的查询
- 浏览器获得IP地址之后,委托操作系统向Web服务器发送请求
- 访问的目标服务器不同,URL的写法也不同;URL打头的是所使用的协议名
- ftp:文件传输协议
- file:本地文件传输协议
- http:超文本传输协议
- mailto:电子邮件协议
- 浏览器解析元素
- URL开头是访问数据源的协议
//
的字符串表示服务器的名称- 之后是资源文件所在的文件路径
- URL省略文件名的情况
- URL的资源路径以
/
结尾,即只明确了文件夹而没有明确文件名,则使用预先设置的默认文件名 - URL的服务器域名后没有明确资源路径,则访问默认文件
- URL的资源路径以
- HTTP思路
- 请求消息 = 操作对象 + 操作内容
- URI(统一资源标识符):操作对象即访问目标
- HTTP方法:对Web服务器的操作
- 响应消息 = 状态码 + 头字端 + 网页数据
- 状态码
- 如果网页中包含图片,会在网页中嵌入图片文件的HTML标签,如
<img src="image.jpeg">
。每条请求消息中只能有一个URL,需要发送多次请求消息来获得多个资源文件
- 状态码
- 常用的两个 HTTP 方法
- GET:向 Web服务器请求资源获取网页数据
- POST:在表单(网页中的文本框等)中填写数据,发送给 Web服务器;此时 URI 会指向 Web服务器中的应用程序的文件名来处理数据
- 请求消息 = 操作对象 + 操作内容
- 请求消息
- 请求行,包括
- HTTP方法:GET/POST
- URI(就是URL中的资源路径)
- HTTP版本号
- 消息头
- 每行包含一个头字段
- 直到空行
- 消息体:包含客户端向 Web服务器发送的数据
- 请求行,包括
- 响应消息
- 状态行
- HTTP版本号
- 状态码
- 响应短语
- 响应时刻、服务器程序类型、数据长度、内容类型等信息
- 资源数据
- 状态行
1.1.2 向DNS服务器查询Web服务器的IP地址
- IP地址 = 网络号 + 主机号
- IP是一串32比特的数字,8比特为一组分为4组
- 网络号对应子网,主机号对应子网中的计算机
- 需要用附加信息——子网掩码,来表示IP地址内部的网络号和主机号
- 主机号全0,表示整个子网,而非某台设备;主机号全1,代表向子网上所有设备发送包——广播
- 让人用服务器名称,让路由器用IP地址,需要用DNS查询服务器名与IP地址之间的对应关系
- 对人来说纯数字的IP地址不好记;对于计算机来说,占用更多字节的域名增加路由器负担
- 操作系统中的 Socket 库中包含了解析器,即DNS客户端,来向DNS服务器发送查询操作,并解析DNS服务器返回的响应信息(Socket 库是用于调用网络功能的程序组件集合)
- 解析器需要委托给操作系统内部的协议栈(调用 Socket 库中的 gethostbyname 组件),来发送查询IP的请求,因为解析器本身不具有网络收发数据的功能
- DNS服务器本身的IP地址不需要查询,是本身就设置好的
1.1.3 全球DNS服务器大接力
- 来自客户端,发给DNS服务器的查询消息,包括
- 域名
- Class:现在永远是代表互联网的 IN
- 记录类型:A记录(域名对应的IP地址),MX记录(域名对应邮件服务器,即@后面的)PTR记录(根据IP地址反查域名),CNAME类型(查询域名相关别名),NS类型(查询DNS服务器IP地址),SOA类型(查询域名属性信息)
- 将信息分布保存在多态DNS服务器中,按照域名,以分层次的结构来保存
- 用
.
来分隔域,越靠右的位置层级越高 - 域名
www.weread.qq.com
,层次即为com
>qq
>weread
>www
- 将负责管理下级域的DNS服务器地址,注册到DNS上级服务器中,以此类推
com
,jp
等顶级域之上还有根域。客户端只要能找到任意一台DNS服务器,都能向上找到根域,再找到下层的目标DNS服务器
- 用
- DNS服务器通过缓存机制来直接响应查询过的IP,而不必从根域开始查找,从而减少查询时间
1.1.4 委托协议栈发送消息
- 与向DNS发送请求信息类似,向 Web服务器发送 HTTP信息也需要向操作系统中的协议栈发出委托,按照指定顺序调用Socket库中的程序组件
- 浏览器委托协议栈代劳,这些操作均通过调用Socket库中的程序组件来执行
- 服务器程序在启动后就创建套接字(socket,即管道两端的接口),进入等待状态,等待客户端连接管道;
- 服务器和客户端双方套接字连接起来之后,通信准备完成;
- 将数据送入套接字,即可收发数据,进行通信;
- 数据发送完毕,链接的管道可以由服务器和客户端的任意一方发起断开。
- 创建套接字
- 调用 Socket 库中的 socket 程序组件
- 套接字创建完成后,协议栈会返回一个描述符,来识别不同的套接字(一台计算机上可能有多个套接字)
- 连接服务器
- 调用 Socket 库中的名为 connect 的程序组件
- 指定三个参数
- 描述符:明确需要使用设备中用于连接的套接字,区分协议栈中的多个套接字
- 服务器 IP 地址:识别网络硬件设备
- 端口号:识别连接对象上具体的套接字
- 通信阶段
- 通过 Socket 库中的 write 程序组件来传递消息,指定套接字,并发送信息
- 通过 Socket 库中的 read 程序组件来接收消息,将响应消息存入接收缓冲区
- 断开阶段
- 调用 close 程序组件来断开连接
- 在 HTTP1.1 中使用了能够在一次连接中收发多个请求和响应的方法,避免了重复连接断开的低效操作
1.2 探索协议栈和网卡
操作系统中的协议栈内部机制
- 用TCP协议收发数据
- 创建套接字
- 连接服务器
- 收发数据
- 从服务器断开连接并删除套接字
- 用IP与以太网的包收发操作
- 用UDP协议收发数据操作
1.2.1 创建套接字
- TCP/IP 软件的分层结构
- 浏览器、邮件等一般应用程序收发数据时用TCP;DNS查询等收发较短的控制数据时用UDP
- 在互联网上传送数据时,数据会被切分成网络包,用IP协议控制网络包的收发
- ICMP:告知网络包传送过程中产生的错误以及各种控制消息
- ARP:根据IP地址查询相应的以太网MAC地址
- 套接字
- 套接字的实体:存放通信控制信息(通信对象的IP、端口号、通信操作)的内存空间
- 协议栈在执行操作时需要参阅控制信息(包括日志和TODO),来判断所需执行的动作
- 套接字内容
- 协议类型:(使用TCP/IP协议)TCP或UDP
- 本地 IP 地址、端口号
- 通信对象 IP 地址、端口号
- 通信状态:LISTINING(等待对方连接)、ESTABLISHED(完成连接,并进行数据通信)
- PID 进程标识符
- 消息收发操作
1.2.2 连接服务器
- 连接的目的
- 将目标服务器的 IP 地址和端口号等控制信息告知协议栈
- 服务器启动时已创建套接字,等待客户端连接,客户端向服务器传达开始通信的请求
- 两种控制信息
- 【头部信息】连接、数据收发、断开连接过程中都需要的,客户端与服务器相互联络交换的控制信息
- 位于网络包头部,被叫做头部;为了区分记作:TCP头部、以太网头部、IP头部
- TCP规格中定义控制信息的字段
- 【套接字信息】保存在套接字中,控制协议栈操作的信息
- 保存:应用程序传来的信息、通信对象接收到的信息、收发数据操作的执行状态等
- 不同的操作系统,协议栈的实现方式不同,所需要的信息也不同,但是IP地址、端口等是共通的
- 【头部信息】连接、数据收发、断开连接过程中都需要的,客户端与服务器相互联络交换的控制信息
- 连接操作的过程
- 提供服务器的IP地址和端口号,调用 Socket 库的 connect 程序组件,将信息传递给协议栈的 TCP模块
- 协议栈中的 TCP 模块会与服务器中的 TCP模块交换控制信息
- 创建一个包含“表示开始数据收发操作的控制信息”的头部,头部中的SYN比特设置为1
- TCP模块将信息传递并委托给IP模块发送,IP模块发送网络包
- 服务器的IP模块将接收到的信息传递给TCP模块
- 服务器的TCP模块根据TCCP头部中的信息找到端口号对应的套接字,将状态改为正在连接
- 服务器的TCP模块会返回响应;需要在TCP头部设置发送方、接收方的端口号,以及SYN比特;将ACK比特设置为1
- 网络包返回客户端,通过IP模块到达TCP模块
- 通过TCP头部的信息确认连接服务器的操作是否成功,SYN比特为1表示成功
- 如果成功,向套接字中写入服务器的IP地址、端口号等,将状态改为连接完毕
- 客户端将ACK比特设置为1,发回服务器。服务器收到响应,连接操作完成
1.2.3 收发数据
- 协议栈收到数据后,执行发送操作
- 协议栈看来,要发送的数据就是一定长度的二进制序列
- 协议栈收到数据,暂存在发送缓冲区中,等待应用程序的下一段数据。满足一定条件之后再发出
- 条件一:长度
- MTU 最大传输单元,表示一个网络包的最大长度
- MSS 最大分段大小,除去头部后,一个网络包能容纳的TCP数据的最大长度
- 条件二:时间
- 条件一:长度
- 发送缓冲区的数据超过MSS,需要对较大的数据进行切分,分块数据放入单独的网络包中。在每一块数据钱加上TCP头部;根据套接字中记录的控制信息标记发送、接收方的端口号
- 使用序号、长度来判断是否丢包;接收端将目前已经接收到的数据长度加起来,作为ACK号,返回给发送方
- TCP采用ACK号来确认对方是否收到了数据,在得到对方确认之前,发送过的包都会保存在发送缓冲区中。如果没收到ACK号,需要重新传输过往包。
1.2.4 断开连接 & 删除套接字
- 断开操作的顺序
- 客户端发送FIN
- 服务器返回ACK号
- 服务器发送FIN
- 客户端返回ACK号
2. 网络编程基础
- 软件结构
- C/S(Client/Server)客户端 —> TCP/IP协议 —> 服务器
- B/S(Browser/Server)浏览器 —> HTTP协议 —> 服务器
- TCP/IP
- Transmission Control Protocol / Internet Protocol,传输控制协议-互联网协议
- 四层分层模式:
- 应用层:HTTP、FTP等
- 传输层:TCP、UDP
- 网络层:IP、ICMP、ARP等
- 数据链路层 + 物理层:底层网络定义的协议
- 网络通信协议
- UDP:
- 无连接通信协议,数据传输时发送端、接收端不建立逻辑连接
- 容易丢包、不能保证数据完整
- 耗资小、效率高,适用于视频通讯但可能卡顿
- TCP
- 面向连接的通信协议,建立逻辑连接之后,再进行数据传输
- 三次握手
- 第一次握手,客户端向服务器端发出连接请求,等待服务器确认;
- 第二次握手,服务器端向客户端回送一个响应,通知客户端收到了连接请求;
- 第三次握手,客户端再次向服务器端发送确认信息,确认连接。
- 三次握手后,建立连接,客户端和服务器才开始进行数据传输,保证数据传输的安全
- UDP:
- 网络编程三要素
- 网络通信协议
- OSI 七层协议
- TCP/IP 四层协议
- 网络通信协议
- IP地址(Internet Protocol Address)
- IPv4:32位地址长度,
.
句点隔开4组十进制数字,/
后表明网络号和主机号的位置 - IPv6:128位地址长度,
:
冒号隔为8组十六进制数字,/
后表明网络号和主机号位置
- IPv4:32位地址长度,
1. 查看本机IP地址:`ipconfig`
2. 本机IP地址:`127.0.0.1`,本地域名:`localhost`
- 端口号
- 网络设备上可能有多个端口,IP地址只能通过网络号和主机号,找到网络设备。需要按照系统指定或人为设定的端口号,找到对应的网络程序
- 逻辑端口号由 2 bytes(8 bits)组成,取值范围是 0~
且不可重复;并且1024之前的端口号不能使用,已经分配出去
3. TCP 通信程序
- TCP网络通信步骤
- 服务器端先启动,等待客户端请求
- 客户端请求服务器,建立逻辑连接——IO对象
- 服务器和客户端使用IO字节流对象进行通信
- Socket是表示客户端的类;Server是表示服务器的类,要包含IP地址和端口号等信息
- 服务器是没有IO流的;
- Server类有accept方法,服务器可获取发出请求的客户端对象
- 服务器在获取客户端对象之后,可使用客户端的字节输入流、字节输出流,与客户端进行数据交互
- 客户端和服务器端进行一次数据交互,需要4个IO流对象
- 客户端发送请求信息:OutputStream
- 服务器接收请求信息:InputStream
- 服务器发送响应信息:OutputStream
- 客户端接收响应信息:InputStream
- java.net.Socket 类
- 套接字 Socket:包含了IP地址、端口号等信息的网络单位,是两网络设备通信的端点
- constructor
Socket(String host, int port)
- host 是主机的域名,或 IP 地址
- port 是主机端口号
- 成员方法
OutputStream getOutStream()
获取套接字的网络字节输出流InputStream getInputStream()
获取套接字的网络字节输入流void close()
关闭套接字,释放 Socket 资源
- java.net.ServerSocket 类
- constructor
ServerSocket(int port)
- 获取请求的客户端对象
Socket accept()
- constructor
4. C/S 综合案例
- C/S结构通信实例```java public class TCPClient { public static void main(String[] args) throws IOException { // 指定IP地址/域名,端口号,创建客户端 Socket socket = new Socket(“localhost”, 1234); // 客户端向服务器发送请求 OutputStream os = socket.getOutputStream(); os.write(“你好服务器!”.getBytes()); // 客户端接收服务器响应 InputStream is = socket.getInputStream(); byte[] bytes = new byte[1024]; int len = is.read(bytes); System.out.println(new String(bytes, 0, len)); // 关闭客户端 socket.close(); } }
public class TCPServer { public static void main(String[] args) throws IOException { // 指定端口号,创建服务器端 ServerSocket server = new ServerSocket(1234); // 获取发送客户端的对象,以及客户端的字节流对象 Socket socket = server.accept(); InputStream is = socket.getInputStream(); OutputStream os = socket.getOutputStream(); // 服务器获取来客户端的请求 byte[] bytes = new byte[1024]; int len = is.read(bytes); System.out.println(new String(bytes, 0, len)); // 使用字节数组生成字符串 // 向客户端发送响应 os.write(“服务器已收到!”.getBytes()); // 关闭服务器 server.close(); } }
2. 文件上传案例```java
public class TCPClient {
public static void main(String[] args) throws IOException {
// 指定IP地址/域名,端口号,创建客户端
Socket socket = new Socket("127.0.0.1", 1234);
// Socket socket = new Socket("localhost", 1234);
InputStream is = socket.getInputStream();
OutputStream os = socket.getOutputStream();
// 使用网络字节流,向服务器发出申请
String request = "来自客户端 " + getCurrentTime() + " 图片文件待上传服务器!";
os.write(request.getBytes());
// 使用网络字节流,获取来自服务器的响应
byte[] bytesOfResponse1 = new byte[1024];
int lenofResponse1 = is.read(bytesOfResponse1);
System.out.println(new String(bytesOfResponse1, 0, lenofResponse1));
// 使用文件字节流读取本地文件;使用网络字节流,向服务器发送文件
String pathName = "TCP_IP_mode.png";
File file = new File(pathName);
FileInputStream fis = new FileInputStream(file);
int bytesOfFile;
while((bytesOfFile=fis.read()) != -1) {
os.write(bytesOfFile);
}
// 结束输出流
socket.shutdownOutput();
// 使用网络字节流,获取来自服务器的响应
byte[] bytesOfResponse2 = new byte[1024];
int lenofResponse2 = is.read(bytesOfResponse2);
System.out.println(new String(bytesOfResponse2, 0, lenofResponse2));
// 关闭所有流对象,以及客户端
socket.close();
fis.close();
}
}
public class TCPServer {
public static void main(String[] args) throws IOException {
// 创建服务器云端环境
File dir = new File("Cloud");
dir.mkdir();
// 指定端口号,创建服务器端
ServerSocket server = new ServerSocket(1234);
// 获取发送客户端的对象,以及客户端的字节流对象
Socket socket = server.accept();
InputStream is = socket.getInputStream();
OutputStream os = socket.getOutputStream();
// 服务器获取来客户端的请求
byte[] bytesOfRequest = new byte[1024];
int lenOfRequest = is.read(bytesOfRequest);
System.out.println(new String(bytesOfRequest, 0, lenOfRequest));
// 服务器向客户端返回响应信息
String response1 = "来自服务器 " + getCurrentTime() + " :准备就绪,客户端请发送!";
os.write(response1.getBytes());
// 服务器创建文件夹及文件
String pathName = "Cloud\\received_file.png";
File file = new File(pathName);
file.createNewFile(); // 创建新文件
FileOutputStream fos = new FileOutputStream(file);
// 将接收到的来自客户端的文件,并写入服务器云端的文件
int len;
byte[] bytes = new byte[1024];
while ((len = is.read(bytes)) != -1) {
fos.write(Arrays.copyOfRange(bytes, 0, len));
}
os.write("上传成功!".getBytes());
// 文件传输结束信息
String response2 = "来自服务器 " + getCurrentTime() + " :已完成文件传输!";
os.write(response2.getBytes());
// 关闭所有流,以及服务器
socket.close();
server.close();
fos.close();
}
}
打印时间信息的方法:```java
public static String getCurrentTime() {
Calendar calendar= Calendar.getInstance();
SimpleDateFormat dateFormat= new SimpleDateFormat(“yyyy-MM-dd :hh:mm:ss”);
return dateFormat.format(calendar.getTime());
}
3. 优化文件上传
1. 自定义文件命名规则:`域名+毫秒值+随机数`
2. 让服务器一直处于监听状态——死循环,`while(true)`
3. 多线程技术:有一个客户端上传文件,就开一个线程完成文件上传
```java
public class TCPClientImproved {
public static void main(String[] args) throws IOException {
// 指定IP地址/域名,端口号,创建客户端
Socket socket = new Socket("localhost", 1234);
InputStream is = socket.getInputStream();
OutputStream os = socket.getOutputStream();
// 使用文件字节流读取本地文件;使用网络字节流,向服务器发送文件
String pathName = "TCP_IP_mode.png";
File file = new File(pathName);
FileInputStream fis = new FileInputStream(file);
int bytes;
while((bytes=fis.read()) != -1) {
os.write(bytes);
}
socket.shutdownOutput(); // 结束输出流
// 关闭所有流对象,以及客户端
socket.close();
fis.close();
}
}
public class TCPServerImproved {
public static void main(String[] args) throws IOException {
// 创建服务器
ServerSocket server = new ServerSocket(1234);
// 创建服务器云端文件夹
String cloudDirName = "Cloud";
File cloudDir = new File(cloudDirName);
if(! cloudDir.exists()) {
cloudDir.mkdir();
}
// 死循环,支持多线程
while(true) {
Socket socket = server.accept();
InputStream is = socket.getInputStream();
OutputStream os = socket.getOutputStream();
// 新建线程,实现文件上传
new Thread(new Runnable() {
@Override
public void run() {
System.out.println(1);
// 待写入文件的命名规则
String suffix = ".png";
String fileName = cloudDirName + "\\image_" + System.currentTimeMillis() + suffix;
System.out.println(fileName);
// 使用try-catch语句,因为Runnable接口中的run方法并没有抛出异常,所以覆盖重写也不能抛出异常
try {
FileOutputStream fos = new FileOutputStream(fileName);
int len;
byte[] bytes = new byte[1024];
while((len = is.read(bytes)) != -1) {
fos.write(bytes, 0, len); // 只写入有效部分
}
socket.close();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}).start(); // 开启线程
}
// 无法在死循环后使用 server.close(); 只能手动关闭服务器
}
}
- 总结
- 如果 Socker 对象的端口号与 ServerSocket 对象的端口号不一致,会报连接异常
java.net.ConnectException: Connection refused: connect
- 客户端读取原文件,不会将文件结束符
-1
传输给服务器,服务器就读不到文件结束符,无法跳出循环。read 方法读不到数据,则进入阻塞状态 - 需要在客户端,使用
socket.shutdownOutput()
通知服务器文件传输完成 - 在写文件的时候,要指定
public void write(byte b[], int off, int len)
方法中的 off 和 len,仅写入有效内容,否则会造成每次都写入字节数组的全部内容
- 如果 Socker 对象的端口号与 ServerSocket 对象的端口号不一致,会报连接异常
5 B/S 案例
浏览器请求服务器的流程
- 在浏览器端输入
http://localhost:8080/web/index.html
- 浏览器发来 “访问服务器上 web/index.html 文件” 的请求
- 服务器应该解析该请求,并且读入 web/index.html 文件,并将响应传给浏览器
- 浏览器显示服务器发送来的数据
- 浏览器解析服务器返回的HTML数据,其中的图像是用URL占位的,如果需要读入图片文件,需要多次服务器响应
public static void main(String[] args) throws IOException {
ServerSocket server = new ServerSocket(8080);
while(true) {
Socket socket = server.accept();
new Thread(new Runnable() {
@Override
public void run() {
try {
InputStream is = socket.getInputStream();
OutputStream os = socket.getOutputStream();
// 解析出浏览器请求访问的文件
BufferedReader br = new BufferedReader(new InputStreamReader(is));
String firstLine = br.readLine();
String fileAddress = firstLine.split(" ")[1].substring(1);
System.out.println(fileAddress);
// 三行固定要返回的响应内容
os.write("HTTP/1.1 200 OK\r\n".getBytes());
os.write("Content-Type:text/html\r\n".getBytes());
os.write("\r\n".getBytes());
// 读取请求访问的文件,并返回响应
FileInputStream fis = new FileInputStream(fileAddress);
int len;
byte[] bytes = new byte[1024];
while((len = fis.read(bytes)) != -1) {
os.write(bytes, 0, len);
}
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}).start();
}
// server.close();
}