1. 《网络是怎样连接的》笔记

1.1 探索浏览器内部

1.1.1 生成HTTP请求

  1. 与Web服务器的交互流程
    1. 用户在浏览器中输入URL(统一资源定位符,即网址)
    2. 浏览器解析URL,根据网址含义生成请求信息,并告知服务器
    3. 浏览器向DNS服务器查询域名对应的IP地址
    4. 全球DNS服务器接力完成IP地址的查询
    5. 浏览器获得IP地址之后,委托操作系统向Web服务器发送请求
  2. 访问的目标服务器不同,URL的写法也不同;URL打头的是所使用的协议名
    1. ftp:文件传输协议
    2. file:本地文件传输协议
    3. http:超文本传输协议
    4. mailto:电子邮件协议
  3. 浏览器解析元素
    1. URL开头是访问数据源的协议
    2. // 的字符串表示服务器的名称
    3. 之后是资源文件所在的文件路径

5. Java 网络编程基础 - 图1

  1. URL省略文件名的情况
    1. URL的资源路径以 / 结尾,即只明确了文件夹而没有明确文件名,则使用预先设置的默认文件名
    2. URL的服务器域名后没有明确资源路径,则访问默认文件
  2. HTTP思路
    1. 请求消息 = 操作对象 + 操作内容
      1. URI(统一资源标识符):操作对象即访问目标
      2. HTTP方法:对Web服务器的操作 5. Java 网络编程基础 - 图2
    2. 响应消息 = 状态码 + 头字端 + 网页数据
      1. 状态码 5. Java 网络编程基础 - 图3
      2. 如果网页中包含图片,会在网页中嵌入图片文件的HTML标签,如 <img src="image.jpeg">。每条请求消息中只能有一个URL,需要发送多次请求消息来获得多个资源文件
    3. 常用的两个 HTTP 方法
      1. GET:向 Web服务器请求资源获取网页数据
      2. POST:在表单(网页中的文本框等)中填写数据,发送给 Web服务器;此时 URI 会指向 Web服务器中的应用程序的文件名来处理数据
  3. 请求消息
    1. 请求行,包括
      1. HTTP方法:GET/POST
      2. URI(就是URL中的资源路径)
      3. HTTP版本号
    2. 消息头
      1. 每行包含一个头字段
      2. 直到空行
    3. 消息体:包含客户端向 Web服务器发送的数据
  4. 响应消息
    1. 状态行
      1. HTTP版本号
      2. 状态码
      3. 响应短语
    2. 响应时刻、服务器程序类型、数据长度、内容类型等信息
    3. 资源数据

1.1.2 向DNS服务器查询Web服务器的IP地址

  1. IP地址 = 网络号 + 主机号
    1. IP是一串32比特的数字,8比特为一组分为4组
    2. 网络号对应子网,主机号对应子网中的计算机
    3. 需要用附加信息——子网掩码,来表示IP地址内部的网络号和主机号 5. Java 网络编程基础 - 图4
    4. 主机号全0,表示整个子网,而非某台设备;主机号全1,代表向子网上所有设备发送包——广播
  2. 让人用服务器名称,让路由器用IP地址,需要用DNS查询服务器名与IP地址之间的对应关系 5. Java 网络编程基础 - 图5
    1. 对人来说纯数字的IP地址不好记;对于计算机来说,占用更多字节的域名增加路由器负担
    2. 操作系统中的 Socket 库中包含了解析器,即DNS客户端,来向DNS服务器发送查询操作,并解析DNS服务器返回的响应信息(Socket 库是用于调用网络功能的程序组件集合)
    3. 解析器需要委托给操作系统内部的协议栈(调用 Socket 库中的 gethostbyname 组件),来发送查询IP的请求,因为解析器本身不具有网络收发数据的功能
    4. DNS服务器本身的IP地址不需要查询,是本身就设置好的

1.1.3 全球DNS服务器大接力

  1. 来自客户端,发给DNS服务器的查询消息,包括
    1. 域名
    2. Class:现在永远是代表互联网的 IN
    3. 记录类型:A记录(域名对应的IP地址),MX记录(域名对应邮件服务器,即@后面的)PTR记录(根据IP地址反查域名),CNAME类型(查询域名相关别名),NS类型(查询DNS服务器IP地址),SOA类型(查询域名属性信息)
  2. 将信息分布保存在多态DNS服务器中,按照域名,以分层次的结构来保存
    1. . 来分隔域,越靠右的位置层级越高
    2. 域名 www.weread.qq.com,层次即为 com > qq > weread > www
    3. 将负责管理下级域的DNS服务器地址,注册到DNS上级服务器中,以此类推
    4. comjp 等顶级域之上还有根域。客户端只要能找到任意一台DNS服务器,都能向上找到根域,再找到下层的目标DNS服务器 5. Java 网络编程基础 - 图6
  3. DNS服务器通过缓存机制来直接响应查询过的IP,而不必从根域开始查找,从而减少查询时间

1.1.4 委托协议栈发送消息

  1. 与向DNS发送请求信息类似,向 Web服务器发送 HTTP信息也需要向操作系统中的协议栈发出委托,按照指定顺序调用Socket库中的程序组件
  2. 浏览器委托协议栈代劳,这些操作均通过调用Socket库中的程序组件来执行
    1. 服务器程序在启动后就创建套接字(socket,即管道两端的接口),进入等待状态,等待客户端连接管道;
    2. 服务器和客户端双方套接字连接起来之后,通信准备完成;
    3. 将数据送入套接字,即可收发数据,进行通信;
    4. 数据发送完毕,链接的管道可以由服务器和客户端的任意一方发起断开。
  3. 创建套接字
    1. 调用 Socket 库中的 socket 程序组件
    2. 套接字创建完成后,协议栈会返回一个描述符,来识别不同的套接字(一台计算机上可能有多个套接字)
  4. 连接服务器
    1. 调用 Socket 库中的名为 connect 的程序组件
    2. 指定三个参数
      1. 描述符:明确需要使用设备中用于连接的套接字,区分协议栈中的多个套接字
      2. 服务器 IP 地址:识别网络硬件设备
      3. 端口号:识别连接对象上具体的套接字
  5. 通信阶段
    1. 通过 Socket 库中的 write 程序组件来传递消息,指定套接字,并发送信息
    2. 通过 Socket 库中的 read 程序组件来接收消息,将响应消息存入接收缓冲区
  6. 断开阶段
    1. 调用 close 程序组件来断开连接
    2. 在 HTTP1.1 中使用了能够在一次连接中收发多个请求和响应的方法,避免了重复连接断开的低效操作

1.2 探索协议栈和网卡

操作系统中的协议栈内部机制

  1. 用TCP协议收发数据
    1. 创建套接字
    2. 连接服务器
    3. 收发数据
    4. 从服务器断开连接并删除套接字
  2. 用IP与以太网的包收发操作
  3. 用UDP协议收发数据操作

1.2.1 创建套接字

  1. TCP/IP 软件的分层结构 5. Java 网络编程基础 - 图7
    1. 浏览器、邮件等一般应用程序收发数据时用TCP;DNS查询等收发较短的控制数据时用UDP
    2. 在互联网上传送数据时,数据会被切分成网络包,用IP协议控制网络包的收发
      1. ICMP:告知网络包传送过程中产生的错误以及各种控制消息
      2. ARP:根据IP地址查询相应的以太网MAC地址
  2. 套接字
    1. 套接字的实体:存放通信控制信息(通信对象的IP、端口号、通信操作)的内存空间
    2. 协议栈在执行操作时需要参阅控制信息(包括日志和TODO),来判断所需执行的动作
  3. 套接字内容
    1. 协议类型:(使用TCP/IP协议)TCP或UDP
    2. 本地 IP 地址、端口号
    3. 通信对象 IP 地址、端口号
    4. 通信状态:LISTINING(等待对方连接)、ESTABLISHED(完成连接,并进行数据通信)
    5. PID 进程标识符
  4. 消息收发操作 5. Java 网络编程基础 - 图8

1.2.2 连接服务器

  1. 连接的目的
    1. 将目标服务器的 IP 地址和端口号等控制信息告知协议栈
    2. 服务器启动时已创建套接字,等待客户端连接,客户端向服务器传达开始通信的请求
  2. 两种控制信息
    1. 【头部信息】连接、数据收发、断开连接过程中都需要的,客户端与服务器相互联络交换的控制信息
      1. 位于网络包头部,被叫做头部;为了区分记作:TCP头部、以太网头部、IP头部
      2. TCP规格中定义控制信息的字段 5. Java 网络编程基础 - 图9
    2. 【套接字信息】保存在套接字中,控制协议栈操作的信息
      1. 保存:应用程序传来的信息、通信对象接收到的信息、收发数据操作的执行状态等
      2. 不同的操作系统,协议栈的实现方式不同,所需要的信息也不同,但是IP地址、端口等是共通的
  3. 连接操作的过程
    1. 提供服务器的IP地址和端口号,调用 Socket 库的 connect 程序组件,将信息传递给协议栈的 TCP模块
    2. 协议栈中的 TCP 模块会与服务器中的 TCP模块交换控制信息
      1. 创建一个包含“表示开始数据收发操作的控制信息”的头部,头部中的SYN比特设置为1
      2. TCP模块将信息传递并委托给IP模块发送,IP模块发送网络包
      3. 服务器的IP模块将接收到的信息传递给TCP模块
      4. 服务器的TCP模块根据TCCP头部中的信息找到端口号对应的套接字,将状态改为正在连接
      5. 服务器的TCP模块会返回响应;需要在TCP头部设置发送方、接收方的端口号,以及SYN比特;将ACK比特设置为1
      6. 网络包返回客户端,通过IP模块到达TCP模块
      7. 通过TCP头部的信息确认连接服务器的操作是否成功,SYN比特为1表示成功
      8. 如果成功,向套接字中写入服务器的IP地址、端口号等,将状态改为连接完毕
      9. 客户端将ACK比特设置为1,发回服务器。服务器收到响应,连接操作完成

1.2.3 收发数据

  1. 协议栈收到数据后,执行发送操作
    1. 协议栈看来,要发送的数据就是一定长度的二进制序列
    2. 协议栈收到数据,暂存在发送缓冲区中,等待应用程序的下一段数据。满足一定条件之后再发出
      1. 条件一:长度
        1. MTU 最大传输单元,表示一个网络包的最大长度
        2. MSS 最大分段大小,除去头部后,一个网络包能容纳的TCP数据的最大长度
      2. 条件二:时间
  2. 发送缓冲区的数据超过MSS,需要对较大的数据进行切分,分块数据放入单独的网络包中。在每一块数据钱加上TCP头部;根据套接字中记录的控制信息标记发送、接收方的端口号
  3. 使用序号、长度来判断是否丢包;接收端将目前已经接收到的数据长度加起来,作为ACK号,返回给发送方 5. Java 网络编程基础 - 图10
  4. TCP采用ACK号来确认对方是否收到了数据,在得到对方确认之前,发送过的包都会保存在发送缓冲区中。如果没收到ACK号,需要重新传输过往包。

1.2.4 断开连接 & 删除套接字

  1. 断开操作的顺序
    1. 客户端发送FIN
    2. 服务器返回ACK号
    3. 服务器发送FIN
    4. 客户端返回ACK号

2. 网络编程基础

  1. 软件结构
    1. C/S(Client/Server)客户端 —> TCP/IP协议 —> 服务器
    2. B/S(Browser/Server)浏览器 —> HTTP协议 —> 服务器
  2. TCP/IP
    1. Transmission Control Protocol / Internet Protocol,传输控制协议-互联网协议
    2. 四层分层模式:
      1. 应用层:HTTP、FTP等
      2. 传输层:TCP、UDP
      3. 网络层:IP、ICMP、ARP等
      4. 数据链路层 + 物理层:底层网络定义的协议
  3. 网络通信协议
    1. UDP:
      1. 无连接通信协议,数据传输时发送端、接收端不建立逻辑连接
      2. 容易丢包、不能保证数据完整
      3. 耗资小、效率高,适用于视频通讯但可能卡顿
    2. TCP
      1. 面向连接的通信协议,建立逻辑连接之后,再进行数据传输
      2. 三次握手
        1. 第一次握手,客户端向服务器端发出连接请求,等待服务器确认;
        2. 第二次握手,服务器端向客户端回送一个响应,通知客户端收到了连接请求;
        3. 第三次握手,客户端再次向服务器端发送确认信息,确认连接。
      3. 三次握手后,建立连接,客户端和服务器才开始进行数据传输,保证数据传输的安全
  4. 网络编程三要素
    1. 网络通信协议
      1. OSI 七层协议
      2. TCP/IP 四层协议

5. Java 网络编程基础 - 图11

  1. IP地址(Internet Protocol Address)
    1. IPv4:32位地址长度,. 句点隔开4组十进制数字,/后表明网络号和主机号的位置
    2. IPv6:128位地址长度,: 冒号隔为8组十六进制数字,/ 后表明网络号和主机号位置

5. Java 网络编程基础 - 图12

  1. 1. 查看本机IP地址:`ipconfig`
  2. 2. 本机IP地址:`127.0.0.1`,本地域名:`localhost`
  1. 端口号
    1. 网络设备上可能有多个端口,IP地址只能通过网络号和主机号,找到网络设备。需要按照系统指定或人为设定的端口号,找到对应的网络程序
    2. 逻辑端口号由 2 bytes(8 bits)组成,取值范围是 0~5. Java 网络编程基础 - 图13 且不可重复;并且1024之前的端口号不能使用,已经分配出去

3. TCP 通信程序

  1. TCP网络通信步骤
    1. 服务器端先启动,等待客户端请求
    2. 客户端请求服务器,建立逻辑连接——IO对象
    3. 服务器和客户端使用IO字节流对象进行通信
  2. Socket是表示客户端的类;Server是表示服务器的类,要包含IP地址和端口号等信息
    1. 服务器是没有IO流的;
    2. Server类有accept方法,服务器可获取发出请求的客户端对象
    3. 服务器在获取客户端对象之后,可使用客户端的字节输入流、字节输出流,与客户端进行数据交互
  3. 客户端和服务器端进行一次数据交互,需要4个IO流对象
    1. 客户端发送请求信息:OutputStream
    2. 服务器接收请求信息:InputStream
    3. 服务器发送响应信息:OutputStream
    4. 客户端接收响应信息:InputStream
  4. java.net.Socket 类
    1. 套接字 Socket:包含了IP地址、端口号等信息的网络单位,是两网络设备通信的端点
    2. constructor Socket(String host, int port)
      1. host 是主机的域名,或 IP 地址
      2. port 是主机端口号
    3. 成员方法
      1. OutputStream getOutStream() 获取套接字的网络字节输出流
      2. InputStream getInputStream() 获取套接字的网络字节输入流
      3. void close() 关闭套接字,释放 Socket 资源
  5. java.net.ServerSocket 类
    1. constructor ServerSocket(int port)
    2. 获取请求的客户端对象 Socket accept()

4. C/S 综合案例

  1. 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. 文件上传案例![](https://cdn.nlark.com/yuque/__mermaid_v3/d11453b2e4758d03da5c0e15dc49f099.svg#code=graph%20BT%0A%0Aa%5Blocal%20file%5D--FileInputStream--%3EC%28Client%29%0AC--OutputStream--%3ES%28Server%29%0AS--InputStream--%3EC%0AS--FileOutputStream--%3Eb%5Bcloud%20file%5D&id=1479703d&type=mermaid)```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(); 只能手动关闭服务器
    }
}
  1. 总结
    1. 如果 Socker 对象的端口号与 ServerSocket 对象的端口号不一致,会报连接异常 java.net.ConnectException: Connection refused: connect
    2. 客户端读取原文件,不会将文件结束符 -1 传输给服务器,服务器就读不到文件结束符,无法跳出循环。read 方法读不到数据,则进入阻塞状态
    3. 需要在客户端,使用 socket.shutdownOutput() 通知服务器文件传输完成
    4. 在写文件的时候,要指定 public void write(byte b[], int off, int len)方法中的 off 和 len,仅写入有效内容,否则会造成每次都写入字节数组的全部内容

5 B/S 案例

浏览器请求服务器的流程

  1. 在浏览器端输入http://localhost:8080/web/index.html
  2. 浏览器发来 “访问服务器上 web/index.html 文件” 的请求
  3. 服务器应该解析该请求,并且读入 web/index.html 文件,并将响应传给浏览器
  4. 浏览器显示服务器发送来的数据
  5. 浏览器解析服务器返回的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();
}