网络原理与网络编程初

网络原理与网络编程初

网络的发展

单机阶段 => 局域网阶段 => 广域网阶段 => 移动互联网阶段

局域网 : 把一些设备通过 交换机 / 路由器 连接起来

广域网 : 把更多的局域网也互相连接 , 当网络规模足够大时

交换机 和路由器 : 组网过程中的重要设备

ip 地址 : 描述一个主机在互联网上的具体位置

端口号 : 区分一个主机上的应用程序

协议

协议就是 “约定” , 发送方约定了他发数据是什么样的格式 , 接收方也得理解这个格式 , 两边对上号才能够进行正确的通信

网络传输本质上都是通过 网线 / 光纤 / 无线 电信号或者光信号来进行传输

网线 : 传输了一系列的高电平和低电平

光纤 : 传输的是光信号

在网络通信中 , 需要约定的协议 , 其实是非常复杂的 , 面对复杂环境 , 就需要复杂协议 , 一个协议太复杂了 , 就可以拆分成多个协议 . 协议是拆分出很多 , 存在有些小的协议 , 作用或者定位是类似的 . 就可以针对这些小协议 , 进行 “分类” , 同时在针对这些不同的类别 , 进行分层.

分层 : 相当于是约定了 层级 和 层级 之间的调用关系 , 要求上层协议调用下层协议 , 下层协议给上层提供支持 , 不能跨层调用

协议分层的好处 :

  1. 分层之后就可以做到 , 层次之间 , 耦合程度比较低 , 上层协议不必了解下层的细节 , 下层也不必了解上层的细节
  2. 方便对某一层的协议进行替换

TCP/IP 五层网络模型

应用层 : 管住传输来的数据 , 是干什么用的

传输层 : 不考虑中间路径 , 只关注起点和终点

比如从家到学校 , 只关心到家和到学校 , 不关心你是做高铁还是飞机

网络层 : 主要负责两个遥远的节点之间 , 路径规划

家在西安 , 学校在北京 , 西安到北京有很多路可以走 , 在众多路径中找一个合适的路径

数据链路层 : 主要关注的是两个相邻节点之间的传输

西安 -> 河南 高铁

河南 -> 北京 飞机

物理层 : 网络通信的基础设施 , 网线 , 光纤, 网络接口

应用层 , 对应程序员写的应用程序

下面的四层 , 则是操作系统内部已经封装好的 , 因此写代码进行网络编程 , 主要工作还是围绕应用层展开的

封装和分用

在协议分层的背景下 , 数据如何通过网络传输 ?

封装和分用

发送方发送数据 , 要把数据从上到下 , 依次交给对应的层次的协议 , 进行封装

接收方收到数据 , 要把数据从下到上 , 依次交给对应的层次的协议 , 进行解析

以 QQ 发送消息为例 , 大概了解下 封装和分用的过程

发送方

A向 B 发送信息 在编辑框输入一条消息 “我爱Java”

  1. 应用层 (QQ应用程序) 拿到上述用户数据 , 进行封装 , 封装成应用层数据包

    数据包 本质就是字符串拼接

    在这里插入图片描述

  2. 传输层拿到上述数据

    应用层要调用传输层提供的 api ,来处理这个数据 , 传输层有很多协议 , 最典型的是 TCP 和 UDP (此处以UDP为例) UDP针对于上述数据包再进行封装

    一个典型的数据报都是通过 报头 + 载荷 的方式构成的

    此处 , UDP协议再给应用层数据报加个UDP报头 , 就是为了再贴一层标签 , 从而在标签上填写必要的属性 . 传输层协议 最关键的属性就是源端口和目的端口了

  3. 传输层到网络层

    UDP 数据报 , 已经有了 , 接下来就是要把这个数据报交给网络层协议 , 网络层最常见的协议 , 就是IP协议

    上述 UDP 数据报 , 到达网络层 , 还需要进一步的封装, 添加上IP协议报头

    源 IP 和目的IP 就描述了这次传输中 ,最初的起点和最终的终点

  4. 网络层交给数据链路层

    最典型的协议 , 叫做以太网 (数据链路层 + 物理层)

在这里插入图片描述

mac 地址 , 也叫做物理地址

也是描述一个主机 , 在网络上的位置

它的功能和 IP 很相似 , 但是 当下就把这两个地址分别作用于不同的用途

IP 用来 进行网络层的路径规划

mac 用来进行描述数据链路层 , 两个即将进行传输的相邻节点

  1. 数据链路层就要把上述以太网数据帧交给物理层了

    物理层要把上述 0101 的二进制数据转换为光信号/电信号/电磁波信号 , 进行传输

### 接收方

上述过程操作系统已经帮我们封装好了

接收过程和上述过程,刚好相反

发送 , 从上到下 , 依次封装 , 新增报头

接收 , 从下到上 , 一次分用 , 去掉报头

忽略中间的转发过程 , 只考虑B的电脑接收到这个消息的情况

  1. 物理层 ,网卡 ,收到高低电平二进制数据

    就会对这里的信号进行解析 , 还原成 0101 这样的二进制序列

  2. 从物理层交给数据链路层

    此时就把上述 010 这系列数据当做一个以太网数据帧 (此处是从以太网线 , 收到的数据, 就是要交给以太网协议来 处理)


    )]

    把 帧头去掉 , 帧尾去掉 ,取出中间的载荷 , 再往上交给网络层

    以太网数据帧帧头中有一个消息类型 , 根据这个类型就知道了网络层ip 协议了

  3. 网络层

    此时就由网络层的ip协议进行解析数据报 ,也是去掉 ip报头

    最重要的还是取出载荷, 交给更上层的传输协议

    IP数据报 , 报头中也有一个字段, 标识当前传输层用的是哪个协议

  4. 传输层

    此处是由UDP来解析处理 , 还是去掉报头, 取出载荷 , 把数据交给应用层

    借助端口号来区分具体的应用程序 , 每个要接受网络数据的程序都需要关联上一个端口号

  5. 应用层

    由QQ 这个程序 , 进行解析应用层数据报 , 取出系列字段 , 放到程序的界面中

总结

发送方 , 层层分装 , 包装快递

接收方 , 层层分用 , 拆快速

真实的网络环境中 , 数据传输中间可能要经历很多节点进行转发

我的电脑(从应用层封装到物理层) ==>交换机 (交换机会进行分用 , 从物理层分用到数据链路层 ,交换机针对这个数据重新封装 , 从数据链路层封装发哦物理层 , 把数据继续转发) ==> 路由器(路由器收到的数据 , 会从物理层用到网络层 , 根据当前得到的目的 IP 进行下一阶段的寻路操作.(IP协议是在一遍传输的过程中 , 一遍规划路径) , 把数据包重新封装 , 从网络层封装到物理层(此时经过数据链路层的时候 , 也会调整mac地址)) ==> 目标电脑 (一直达到这个目标主机 , 才会完成从物理层分用到应用层这个过程).

每次消息的传输 , 都会涉及封装分用

套接字(socket)

程序员写网络程序 , 主要编写的应用层代码 . 真正要发送这个数据 , 需要上层协议调用下层协议. 应用层要调用传输层

传输层给应用层提供一组 api 统称为 socket api

系统给我们提供的 socket api 主要是两组 :

  1. 基于 UDP 的 api
  2. 基于 TCP 的 api

TCP 和 UDP 这俩协议差别很大 , 提供的api 也差异很大

这两个协议各自的特点:

UDP :

  1. 无连接 使用 UDP 通信的双方 , 不需要刻意保存对端的相关信息
  2. 不可靠传输 消息发送 , 不关注结果
  3. 面向数据报 以一个UDP 数据报为基本单位
  4. 全双工 一条路径 , 双向通信

TCP:

  1. 有连接 使用TCP通信的双方 , 则需要刻意保存对方的相关信息
  2. 可靠传输 发了并不能100%到达对方 , 尽可能的传输过去
  3. 面向字节流 以字节为传输的基本单位 , 读写方式非常灵活
  4. 全双工 一条路径 , 双向通信

使用这两个 socket 的时候 , 就不必考虑单向通信的问题

全双工 : 双向通信 打电话

半双工 : 单向通信 对讲机

UDP的 api

DatagramSocket API

DatagramSocket 是UDP Socket . 用于发送和接收 UDP数据报

Datagram 就是"数据报" , Socket 书名这个对象是一个 socket 对象(相当于 对应到系统中一个特殊文件(socket文件) socket 文件并非对应到硬盘上的某个数据存储区域 , 而是对应到 ,网卡这个硬件设备)

所以 , 要想进行网络通信 , 就需要有 socket 文件这样的对象 , 借助这个 socket 文件对象 , 才能够间接的操作网卡

往这个socket 对象中写数据 , 相当于通过网卡发送消息

从这个socket 对象中读数据 , 相当于通过网卡接收消息

DatagramSocket 构造方法:

方法名方法说明
DatagramSocket()创建一个UDP数据报套接字 Socket .绑定到本机任意一个随机端口(一般用于客户端)
DatagramSocket(int port)创建一个UDP数据报套接字的Socket .绑定到本机指定的端口(一般用于服务器)

此处的 Socket 对象可能被客户端/服务器都使用 . 服务器这边的socket 往往要关联一个具体的端口号(必须不变) , 客户端这边则不需要 手动指定 , 系统自动分配即可 (不要求)

DatagramSocket 方法:

方法名方法说明
void receive(DatagramPacket p)从此套接字接收到数据报(如果没有接收到数据报 , 该方法会阻塞等待)
void send(DatagramPacket p)从此套接字发送数据报(不会阻塞等待 , 直接发送)
void close()关闭此数据报套接字

socket 也是文件 , 文件用完了就需要关闭 , 否则就会出现文件资源泄露问题

DatagramPacket API

DatagramPacket 是 UDP Socket 发送和接收的数据报

DatagramPacket 构造方法

方法名方法说明
DatagramPacket(byte[] buf,int length)构造一个DatagramPacket以用来接收数据报,接收的数据保存在
字节数组(第一个参数buf)中,接收指定长度(第二个参数
length)
DatagramPacket(byte[] buf , int offset,int length,SocketAddress address)构造一个DatagramPacket以用来发送数据报,发送的数据为字节
数组(第一个参数buf)中,从0到指定长度(第二个参数
length)。address指定目的主机的IP和端口号

DatagramPacket(byte[] buf,int length) :这个版本不需要设置地址进去 , 通常用来接收信息

DatagramPacket(byte[] buf , int offset,int length,SocketAddress address) : 这个版本 需要显式的设置地址进去 , 通常要用来发送消息

DatagramPacket 方法:

方法名方法说明
InetAddress getAddress()从接收的数据报中, 获取发送端主机的IP地址 ; 或从发送的数据报中,获取接收端主机IP地址
int getPort()从接收的数据报中 , 获取发送端主机的端口号 ; 或从发送的数据报中 , 获取接收端主机端口号
byte[] getData()获取数据报中的数据

基于 UDP socket 简单的客户端 服务器程序

回显服务器(echo sever)

客户端发了个请求 , 服务器返回一个一模一样的响应

一个服务器 , 主要要做三个核心工作:

  1. 读取请求分析并解析
  2. 根据请求计算响应(省略)
  3. 把响应返回到客户端

服务器:

package network;

import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.SocketException;

public class UdpEchoServer {
    // 需要先定义一个 socket 对象.
    // 通过网络通信, 必须要使用 socket 对象.
    private DatagramSocket socket = null;

    // 绑定一个端口, 不一定能成功!!
    // 如果某个端口已经被别的进程占用了, 此时这里的绑定操作就会出错.
    // 同一个主机上, 一个端口, 同一时刻, 只能被一个进程绑定.
    public UdpEchoServer(int port) throws SocketException {
        // 构造 socket 的同时, 指定要关联/绑定的端口.
        socket = new DatagramSocket(port);
    }

    // 启动服务器的主逻辑.
    public void start() throws IOException {
        System.out.println("服务器启动!");
        while (true) {
            // 每次循环, 要做三件事情:
            // 1. 读取请求并解析
            //    构造空饭盒
            DatagramPacket requestPacket = new DatagramPacket(new byte[4096], 4096);
            //    食堂大妈给饭盒里盛饭. (饭从网卡上来的)
            socket.receive(requestPacket);
            //    为了方便处理这个请求, 把数据包转成 String
            String request = new String(requestPacket.getData(), 0, requestPacket.getLength());
            // 2. 根据请求计算响应(此处省略这个步骤)
            String response = process(request);
            // 3. 把响应结果写回到客户端
            //    根据 response 字符串, 构造一个 DatagramPacket .
            //    和请求 packet 不同, 此处构造响应的时候, 需要指定这个包要发给谁.
            DatagramPacket responsePacket = new DatagramPacket(response.getBytes(), response.getBytes().length,
                    // requestPacket 是从客户端这里收来的. getSocketAddress 就会得到客户端的 ip 和 端口
                    requestPacket.getSocketAddress());
            socket.send(responsePacket);
            System.out.printf("[%s:%d] req: %s, resp: %s\n", requestPacket.getAddress().toString(),
                    requestPacket.getPort(), request, response);
        }
    }

    // 这个方法希望是根据请求计算响应.
    // 由于咱们写的是个 回显 程序. 请求是啥, 响应就是啥!!
    // 如果后续写个别的服务器, 不再回显了, 而是有具体的业务了, 就可以修改 process 方法,
    // 根据需要来重新构造响应.
    // 之所以单独列成一个方法, 就是想让同学们知道, 这是一个服务器中的关键环节!!!
    private String process(String request) {
        return request;
    }

    public static void main(String[] args) throws IOException {
        UdpEchoServer udpEchoServer = new UdpEchoServer(9090);
        udpEchoServer.start();
    }
}

如果客户端 发来的是数据是 “老板 来碗面” , 此时这个数据就会以二进制的形式躺在 requestPacket 中的字节数组中 , 把这个字节数组拿出来 , 重新构造一个 String , 这个String 的内容就是 “老板 来碗面”

客户端:

package network;

import java.io.IOException;
import java.net.*;
import java.util.Scanner;

public class UdpEchoClient {
    private DatagramSocket socket = null;
    private String serverIP;
    private int serverPort;

    // 客户端启动, 需要知道服务器在哪里!!
    public UdpEchoClient(String serverIP, int serverPort) throws SocketException {
        // 对于客户端来说, 不需要显示关联端口.
        // 不代表没有端口, 而是系统自动分配了个空闲的端口.
        socket = new DatagramSocket();
        this.serverIP = serverIP;
        this.serverPort = serverPort;
    }

    public void start() throws IOException {
        // 通过这个客户端可以多次和服务器进行交互.
        Scanner scanner = new Scanner(System.in);
        while (true) {
            // 1. 先从控制台, 读取一个字符串过来
            //    先打印一个提示符, 提示用户要输入内容
            System.out.print("-> ");
            String request = scanner.next();
            // 2. 把字符串构造成 UDP packet, 并进行发送.
            DatagramPacket requestPacket = new DatagramPacket(request.getBytes(), request.getBytes().length,
                    InetAddress.getByName(serverIP), serverPort);
            socket.send(requestPacket);
            // 3. 客户端尝试读取服务器返回的响应
            DatagramPacket responsePacket = new DatagramPacket(new byte[4096], 4096);
            socket.receive(responsePacket);
            // 4. 把响应数据转换成 String 显示出来.
            String response = new String(responsePacket.getData(), 0, responsePacket.getLength());
            System.out.printf("req: %s, resp: %s\n", request, response);
        }
    }

    public static void main(String[] args) throws IOException {
        // UdpEchoClient udpEchoClient = new UdpEchoClient("127.0.0.1", 9090);
        UdpEchoClient udpEchoClient = new UdpEchoClient("42.192.83.143", 9090);
        udpEchoClient.start();
    }
}

这个构造 , 也是吧数据构造成 DatagramPacket ,一方面需要 String 中的 getBytes 数组 , 另一方面 , 需要指定服务器的ip 和 端口 . 此处不是通过 InetAddress 直接构造了 ,而是分开设置

服务器返回响应的时候, 是直接从 packet 里面取出的 InetAddress 对象

总结

  1. 服务器先启动 , 执行到 receive 进行阻塞

  2. 客户端运行之后 , 从控制台读取数据, 并进行send

  3. 客户端这边 , send 之后 , 继续往下走 , 走到 receive 读取响应 , 会阻塞等待

    服务器这边 , 就从 receive 返回 , 读到请求数据(客户端发来的) , 往下走到 process 生成响应 ,再往下走到 send , 并且打印日志

  4. 客户端这边真正收到服务器send 回来的数据后 , 就会解除阻塞 , 执行下面的打印操作

    服务器这边进入下一轮循环 , 再次阻塞在 receive , 等待客户端下一个请求

  5. 客户端继续进入下一轮循环 , 阻塞在 Scanner.next 这里 , 等待用户输入新的数据

虽然上述流程 , 初步看起来比较复杂 , 但是所有的服务器客户端程序基本上都是这样的

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值