9、网络编程


一、网络编程基础知识

计算机网络是指两台或更多的计算机组成的网络,在同一个网络中,任意两台计算机都可以直接通信,因为所有计算机都需要遵循同一种网络协议。
那什么是互联网呢?互联网是网络的网络(internet),即把很多计算机网络连接起来,形成一个全球统一的互联网。
对某个特定的计算机网络来说,它可能使用网络协议 ABC,而另一个计算机网络可能使用网络协议 XYZ。如果计算机网络各自的通讯协议不统一,就没法把不同的网络连接起来形成互联网。因此,为了把计算机网络接入互联网,就必须使用 TCP/IP 协议。
TCP/IP 协议泛指互联网协议,其中最重要的两个协议是 TCP 协议和 IP 协议。只有使用 TCP/IP 协议的计算机才能够联入互联网,使用其他网络协议(例如 NetBIOS、AppleTalk 协议等)是无法联入互联网的。

1.IP 地址

在互联网中,一个 IP 地址用于唯一标识一个网络接口(Network Interface)。一台联入互联网的计算机肯定有一个 IP 地址,但也可能有多个 IP 地址。
IP 地址分为 IPv4 和 IPv6 两种。IPv4 采用 32 位地址,类似101.202.99.12,而 IPv6 采用 128 位地址,类似2001:0DA8:100A:0000:0000:1020:F2F3:1428。IPv4 地址总共有 232 个(大约 42 亿),而 IPv6 地址则总共有 2128 个(大约 340 万亿亿亿亿),IPv4 的地址目前已耗尽,而 IPv6 的地址是根本用不完的。
IP 地址又分为公网 IP 地址和内网 IP 地址。公网 IP 地址可以直接被访问,内网 IP 地址只能在内网访问。内网 IP 地址类似于:

  • 192.168.x.x
  • 10.x.x.x

有一个特殊的 IP 地址,称之为本机地址,它总是127.0.0.1
IPv4 地址实际上是一个 32 位整数。例如:

106717964 = 0x65ca630c
          = 65  ca  63 0c
          = 101.202.99.12

如果一台计算机只有一个网卡,并且接入了网络,那么,它有一个本机地址127.0.0.1,还有一个 IP 地址,例如101.202.99.12,可以通过这个 IP 地址接入网络。
如果一台计算机有两块网卡,那么除了本机地址,它可以有两个 IP 地址,可以分别接入两个网络。通常连接两个网络的设备是路由器或者交换机,它至少有两个 IP 地址,分别接入不同的网络,让网络之间连接起来。
如果两台计算机位于同一个网络,那么他们之间可以直接通信,因为他们的 IP 地址前段是相同的,也就是网络号是相同的。网络号是 IP 地址通过子网掩码过滤后得到的。例如:
某台计算机的 IP 是101.202.99.2,子网掩码是255.255.255.0,那么计算该计算机的网络号是:

IP = 101.202.99.2
Mask = 255.255.255.0
Network = IP & Mask = 101.202.99.0

每台计算机都需要正确配置 IP 地址和子网掩码,根据这两个就可以计算网络号,如果两台计算机计算出的网络号相同,说明两台计算机在同一个网络,可以直接通信。如果两台计算机计算出的网络号不同,那么两台计算机不在同一个网络,不能直接通信,它们之间必须通过路由器或者交换机这样的网络设备间接通信,我们把这种设备称为网关。
网关的作用就是连接多个网络,负责把来自一个网络的数据包发到另一个网络,这个过程叫路由。
所以,一台计算机的一个网卡会有 3 个关键配置:

  • IP 地址,例如:10.0.2.15
  • 子网掩码,例如:255.255.255.0
  • 网关的 IP 地址,例如:10.0.2.2

2.域名

因为直接记忆 IP 地址非常困难,所以我们通常使用域名访问某个特定的服务。域名解析服务器 DNS 负责把域名翻译成对应的 IP,客户端再根据 IP 地址访问服务器。
nslookup可以查看域名对应的 IP 地址:

$ nslookup www.liaoxuefeng.com
Server:  xxx.xxx.xxx.xxx
Address: xxx.xxx.xxx.xxx#53
Non-authoritative answer:
Name:    www.liaoxuefeng.com
Address: 47.98.33.223

有一个特殊的本机域名localhost,它对应的 IP 地址总是本机地址127.0.0.1

3.网络模型

由于计算机网络从底层的传输到高层的软件设计十分复杂,要合理地设计计算机网络模型,必须采用分层模型,每一层负责处理自己的操作。OSI(Open System Interconnect 开放式系统互联)网络模型是 ISO 组织定义的一个计算机互联的标准模型,注意它只是一个定义,目的是为了简化网络各层的操作,提供标准接口便于实现和维护。这个模型从上到下依次是:

  • 应用层,提供应用程序之间的通信;
  • 表示层:处理数据格式,加解密等等;
  • 会话层:负责建立和维护会话;
  • 传输层:负责提供端到端的可靠传输;
  • 网络层:负责根据目标地址选择路由来传输数据;
  • 链路层和物理层负责把数据进行分片并且真正通过物理网络传输,例如,无线网、光纤等。

互联网实际使用的 TCP/IP 模型并不是对应到 OSI 的 7 层模型,而是大致对应 OSI 的 5 层模型:

  • TCP(Transmission Control Protocol)传输控制协议
    传输基本单位是报文段

应用范围:运输层,进程之间

  • UDP(User Datagram Protocol)用户数据报协议
    传输基本单位是用户数据报

应用范围:运输层,不同主机上的进程之间

  • IP(Internet Protocol)协议
    用于网络层,主要为不同的主机之间的数据传输提供服务

服务与协议之间的关系
协议是对等的,服务是垂直的。
协议的作用是保证不同实体之间的传输是对等的,而不同层之间,通过“服务”来使下层给上层暴露出有限的接口,即层间接口,使得上层能够使用下层的服务,来完成协议的功能
参考:https://www.cnblogs.com/bqwzx/p/11053778.html

4.常用协议

IP 协议是一个分组交换,它不保证可靠传输。而 TCP 协议是传输控制协议,它是面向连接的协议,支持可靠传输和双向通信。TCP 协议是建立在 IP 协议之上的,简单地说,IP 协议只负责发数据包,不保证顺序和正确性,而 TCP 协议负责控制数据包传输,它在传输数据之前需要先建立连接,建立连接后才能传输数据,传输完后还需要断开连接。TCP 协议之所以能保证数据的可靠传输,是通过接收确认、超时重传这些机制实现的。并且,TCP 协议允许双向通信,即通信双方可以同时发送和接收数据。
TCP 协议也是应用最广泛的协议,许多高级协议都是建立在 TCP 协议之上的,例如 HTTP、SMTP 等。
UDP 协议(User Datagram Protocol)是一种数据报文协议,它是无连接协议,不保证可靠传输。因为 UDP 协议在通信前不需要建立连接,因此它的传输效率比 TCP 高,而且 UDP 协议比 TCP 协议要简单得多。
选择 UDP 协议时,传输的数据通常是能容忍丢失的,例如,一些语音视频通信的应用会选择 UDP 协议。

5.小结

计算机网络的基本概念主要有:

  • 计算机网络:由两台或更多计算机组成的网络;
  • 互联网:连接网络的网络;
  • IP 地址:计算机的网络接口(通常是网卡)在网络中的唯一标识;
  • 网关:负责连接多个网络,并在多个网络之间转发数据的计算机,通常是路由器或交换机;
  • 网络协议:互联网使用 TCP/IP 协议,它泛指互联网协议簇;
  • IP 协议:一种分组交换传输协议;
  • TCP 协议:一种面向连接,可靠传输的协议;
  • UDP 协议:一种无连接,不可靠传输的协议。

二、TCP 通信

TCP 通信能实现两台计算机之间的数据交互,通信的两端,要严格区分为客户端(Client)与服务端(Server)。
两端通信时步骤:

  1. 服务端程序,需要事先启动,等待客户端的连接。
  2. 客户端主动连接服务器端,连接成功才能通信。服务端不可以主动连接客户端。

在 Java 中,提供了两个类用于实现 TCP 通信程序:

  1. 客户端:java.net.Socket 类表示。创建Socket对象,向服务端发出连接请求,服务端响应请求,两者建立连接开始通信。
  2. 服务端:java.net.ServerSocket 类表示。创建ServerSocket对象,相当于开启一个服务,并等待客户端的连接。

1.Socket 类

构造方法

  • public Socket(String host, int port) :创建套接字对象并将其连接到指定主机上的指定端口号。如果指定的 host 是 null ,则相当于指定地址为回送地址。

    小贴士:回送地址(127.x.x.x) 是本机回送地址(Loopback Address),主要用于网络软件测试以及本地机进程间通信,无论什么程序,一旦使用回送地址发送数据,立即返回,不进行任何网络传输。

构造举例,代码如下:

Socket client = new Socket("127.0.0.1", 6666);

成员方法

  • public InputStream getInputStream() : 返回此套接字的输入流。
    • 如果此 Scoket 具有相关联的通道,则生成的 InputStream 的所有操作也关联该通道。
    • 关闭生成的 InputStream 也将关闭相关的 Socket。
  • public OutputStream getOutputStream() : 返回此套接字的输出流。
    • 如果此 Scoket 具有相关联的通道,则生成的 OutputStream 的所有操作也关联该通道。
    • 关闭生成的 OutputStream 也将关闭相关的 Socket。
  • public void close() :关闭此套接字。
    • 一旦一个 socket 被关闭,它不可再使用。
    • 关闭此 socket 也将关闭相关的 InputStream 和 OutputStream 。
  • public void shutdownOutput() : 禁用此套接字的输出流。
    • 任何先前写出的数据将被发送,随后终止输出流。

2.ServerSocket 类

ServerSocket类:这个类实现了服务器套接字,该对象等待通过网络的请求。

构造方法

  • public ServerSocket(int port) :使用该构造方法在创建 ServerSocket 对象时,就可以将其绑定到一个指定的端口号上,参数 port 就是端口号。

构造举例,代码如下:

ServerSocket server = new ServerSocket(6666);

成员方法

  • public Socket accept() :侦听并接受连接,返回一个新的 Socket 对象,用于和客户端实现通信。该方法会一直阻塞直到建立连接。

3.TCP 通信分析图解

  1. 【服务端】启动,创建 ServerSocket 对象,等待连接。
  2. 【客户端】启动,创建 Socket 对象,请求连接。
  3. 【服务端】接收连接,调用 accept 方法,并返回一个 Socket 对象。
  4. 【客户端】Socket 对象,获取 OutputStream,向服务端写出数据。
  5. 【服务端】Scoket 对象,获取 InputStream,读取客户端发送的数据。

    到此,客户端向服务端发送数据成功。

自此,服务端向客户端回写数据。

  1. 【服务端】Socket 对象,获取 OutputStream,向客户端回写数据。
  2. 【客户端】Scoket 对象,获取 InputStream,解析回写数据。
  3. 【客户端】释放资源,断开连接。

服务器端实现

public class ServerTcp {
    public static void main(String[] args) throws IOException {
        //1.创建服务端对象
        ServerSocket ss = new ServerSocket(7788);
        //2.监听客户端连接,并获得套接字对象
        System.out.println("等待客户端连接>>>");
        Socket s = ss.accept();
        //3.获得输入流,读数据,并把数据显示在控制台
        InputStream is = s.getInputStream();
        //int len = read(byte[] b)   从输入流读取字节数,并将它们存储到缓冲区 b,返回输入流字节长度
        byte[] bys = new byte[1024];
        int len = is.read(bys);
        String data = new String(bys,0,len);
        System.out.println("服务端接收到的数据是:"+data);
        //3.获得输出流向客户端发送数据
        OutputStream outputStream = s.getOutputStream();
        outputStream.write(data.toUpperCase().getBytes());
        //4.释放资源
        s.close();  //可以不用写
        ss.close();
    }
}

客户端实现:

public class ClientTcp {
    public static void main(String[] args) throws IOException {
        //1.创建socket对象
        Socket s = new Socket("127.0.0.1",7788);
        //2.获得输出流,写数据
        OutputStream outputStream = s.getOutputStream();
        outputStream.write("我是tcp客户端,我非常喜欢java编程".getBytes());
        //2.获得输入流,接收数据
        InputStream inputStream = s.getInputStream();
        byte[] bys = new byte[1024];
        int len = inputStream.read(bys);
        System.out.println("收到服务端的回复:"+new String(bys,0,len));
        //3.释放资源
        s.close();
    }
}

4.TCP 案例练习

练习 1

  • 客户端:发送数据,接收服务端反馈
  • 服务端:接收数据,给出反馈

练习 2

  • 客户端:数据来自键盘录入,直到输入的数据是 886,发送数据结束
  • 服务端:接收到的数据在控制台输出

练习 3

  • 客户端:数据来自键盘录入,直到输入的数据是 886,发送数据结束
  • 服务端:接收到的数据写入文本文件

练习 4(上传文件)

  • 客户端:数据来自文本文件
  • 服务端:接收到的数据写入文本文件

文件上传案例原理分析:

文件上传分析图解:

  1. 【客户端】输入流,从硬盘读取文件数据到程序中。
  2. 【客户端】输出流,写出文件数据到服务端。
  3. 【服务端】输入流,读取文件数据到服务端程序。
  4. 【服务端】输出流,写出文件数据到服务器硬盘中。


客户端实现:

public class UploadClient {
    public static void main(String[] args) throws IOException {
        //1.
        Socket sock = new Socket("127.0.0.1",7788);
        //2.准备数据源
        BufferedInputStream bis = new BufferedInputStream(new FileInputStream("./1.jpg"));
        //3.输出流
        BufferedOutputStream bos = new BufferedOutputStream(sock.getOutputStream());
        byte[] bys = new byte[1024];
        int len=0;
        while ((len = bis.read(bys))!= -1){
            bos.write(bys,0,len);
            bos.flush();
        }
        //注意:上传成功如果没有结束标记,可以使用:shutdownOutput()  禁用此套接字的输出流。
        System.out.println("文件上传成功!");
        //4.
        sock.close();
    }
}

服务器端实现:

public class UploadServer {
    public static void main(String[] args) throws IOException {
        ServerSocket ss = new ServerSocket(7788);
        Socket s = ss.accept();
        //输入流-获取客户端发送的数据
        BufferedInputStream bis = new BufferedInputStream(s.getInputStream());
        //输出流-保存文件到本地
        BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("./copy1.jpg"));
        //读写数据
        byte[] b = new byte[1024];
        int len = 0;
        while ((len = bis.read(b))!=-1){
            bos.write(b,0,len);
            bos.flush();
        }
        System.out.println("服务端收到文件");
        //关闭
        s.close();
        bis.close();    //记得要关闭文件
        ss.close();
    }
}

文件上传优化分析:

  1. 文件名称写死的问题
    服务端,保存文件的名称如果写死,那么最终导致服务器硬盘,只会保留一个文件,建议使用系统时间优化,保证文件名称唯一,代码如下:
//准备文件夹
File f1 = new File("./upload");

if(!(f1.exists())){
f1.mkdirs();
}
//不重复的文件名
String filename = “xjt”+System.currentTimeMillis()+ new Random().nextInt(100)+”.mkv”;
BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(f1+”//“+filename));

2. **循环接收的问题**
服务端,指保存一个文件就关闭了,之后的用户无法再上传,这是不符合实际的,使用循环改进,可以不断的接收不同用户的文件,代码如下:
```java
// 每次接收新的连接,创建一个Socket
while(true){
    Socket accept = serverSocket.accept();
    ......
}
  1. 多线程提高效率
    服务端,在接收大文件时,可能耗费几秒钟的时间,此时不能接收其他用户上传,所以,使用多线程技术优化,代码如下:
whiletrue{
    Socket accept = serverSocket.accept();
    // accept 交给子线程处理.
    new Thread(() -> {
      	......
        InputStream bis = accept.getInputStream();
      	......
    }).start();
}

优化实现-服务端

public class UploadServer {
    public static void main(String[] args) throws IOException {
        ServerSocket ss = new ServerSocket(7788);
        while(true){
            Socket s = ss.accept();
            //线程池
            ExecutorService ec = Executors.newFixedThreadPool(10);
            ec.submit(() ->{
                try{
                    //输入流-获取客户端发送的数据
                    BufferedInputStream bis = new BufferedInputStream(s.getInputStream());
                    //输出流-保存文件到本地
                    File f1 = new File("./upload");
                    if(!(f1.exists())){
                        f1.mkdirs();
                    }
                    //文件名称不重复
                    String filename = "xjt"+System.currentTimeMillis()+ new Random().nextInt(100)+".mkv";
                    BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(f1+"//"+filename));
                    //读写数据
                    byte[] b = new byte[1024];
                    int len = 0;
                    while ((len = bis.read(b))!=-1){
                        bos.write(b,0,len);
                        bos.flush();
                    }
                    System.out.println("服务端收到文件,当前线程是:"+Thread.currentThread().getName());
                    //关闭
                    s.close();
                    bis.close();    //记得要关闭文件
                }catch (Exception e){
                    e.printStackTrace();
                }
            });
        }
        //关闭ServerSocket对象
        //ss.close();   //这里死循环就不用关了
    }
}

练习 5

  • 客户端:数据来自文本文件,接收服务端反馈
  • 服务端:接收到的数据写入文本文件,给出反馈

    出现问题:程序一直等待
    原因:读数据的方法是阻塞式的
    解决办法:自定义结束标记,使用 shutdownOutput() 方法

练习 6

  • 客户端:数据来自文本文件,接收服务端反馈
  • 服务端:接收到的数据写入文本文件,给出反馈,代码用线程进行封装,为每一个客户端开启一个线程


练习 6
模拟网站服务器,使用浏览器访问自己编写的服务端程序,查看网页效果。
案例分析:

  1. 准备页面数据,web 文件夹。
    复制到我们 Module 中,比如复制到 day08 中

  2. 我们模拟服务器端,ServerSocket 类监听端口,使用浏览器访问

  3. 服务器程序中字节输入流可以读取到浏览器发来的请求信息

GET/web/index.html HTTP/1.1 是浏览器的请求消息。/web/index.html 为浏览器想要请求的服务器端的资源,使用字符串切割方式获取到请求的资源。

//转换流,读取浏览器请求第一行
BufferedReader readWb = new BufferedReader(new InputStreamReader(socket.getInputStream()));
String requst = readWb.readLine();
//取出请求资源的路径
String[] strArr = requst.split(" ");
//去掉web前面的/
String path = strArr[1].substring(1);
System.out.println(path);

案例实现
服务端实现:

public class SerDemo {
    public static void main(String[] args) throws IOException {
        System.out.println("服务端  启动 , 等待连接 .... ");
        // 创建ServerSocket 对象
        ServerSocket server = new ServerSocket(8888);
        Socket socket = server.accept();
        // 转换流读取浏览器的请求消息
        BufferedReader readWb = new
        BufferedReader(new InputStreamReader(socket.getInputStream()));
        String requst = readWb.readLine();
        // 取出请求资源的路径
        String[] strArr = requst.split(" ");
        // 去掉web前面的/
        String path = strArr[1].substring(1);
        // 读取客户端请求的资源文件
        FileInputStream fis = new FileInputStream(path);
        byte[] bytes= new byte[1024];
        int len = 0 ;
        // 字节输出流,将文件写会客户端
        OutputStream out = socket.getOutputStream();
        // 写入HTTP协议响应头,固定写法
        out.write("HTTP/1.1 200 OK\r\n".getBytes());
        out.write("Content-Type:text/html\r\n".getBytes());
        // 必须要写入空行,否则浏览器不解析
        out.write("\r\n".getBytes());
        while((len = fis.read(bytes))!=-1){
            out.write(bytes,0,len);
        }
        fis.close();
        out.close();
        readWb.close();
        socket.close();
        server.close();
    }
}

访问效果

  • 火狐

小贴士:不同的浏览器,内核不一样,解析效果有可能不一样。

发现浏览器中出现很多的叉子,说明浏览器没有读取到图片信息导致。
浏览器工作原理是遇到图片会开启一个线程进行单独的访问,因此在服务器端加入线程技术。

public class ServerDemo {
    public static void main(String[] args) throws IOException {
        ServerSocket server = new ServerSocket(8888);
        while(true){
            Socket socket = server.accept();
            new Thread(new Web(socket)).start();
        }
    }
    static class Web implements Runnable{
        private Socket socket;
        public Web(Socket socket){
            this.socket=socket;
        }
        public void run() {
            try{
                //转换流,读取浏览器请求第一行
                BufferedReader readWb = new
                        BufferedReader(new InputStreamReader(socket.getInputStream()));
                String requst = readWb.readLine();
                //取出请求资源的路径
                String[] strArr = requst.split(" ");
                System.out.println(Arrays.toString(strArr));
                String path = strArr[1].substring(1);
                System.out.println(path);
                FileInputStream fis = new FileInputStream(path);
                System.out.println(fis);
                byte[] bytes= new byte[1024];
                int len = 0 ;
                //向浏览器 回写数据
                OutputStream out = socket.getOutputStream();
                out.write("HTTP/1.1 200 OK\r\n".getBytes());
                out.write("Content-Type:text/html\r\n".getBytes());
                out.write("\r\n".getBytes());
                while((len = fis.read(bytes))!=-1){
                    out.write(bytes,0,len);
                }
                fis.close();
                out.close();
                readWb.close();
                socket.close();
            }catch(Exception ex){
            }
        }
    }
}

访问效果:
图解:


三、UDP 编程

1.UDP 发送/接收数据基本步骤

发送:

接收:

DatagramPacket构造方法:

DatagramPacket(byte[] buf, int length)
	构造一个 DatagramPacket用于接收长度的数据包 length 。
DatagramPacket(byte[] buf, int length, InetAddress address, int port)
	构造用于发送长度的分组的数据报包 length指定主机上到指定的端口号。
DatagramPacket(byte[] buf, int offset, int length)
	构造一个 DatagramPacket用于接收长度的分组 length ,指定偏移到缓冲器中。
DatagramPacket(byte[] buf, int offset, int length, InetAddress address, int port)
	构造用于发送长度的分组数据报包 length具有偏移 ioffset指定主机上到指定的端口号。
DatagramPacket(byte[] buf, int offset, int length, SocketAddress address)
	构造用于发送长度的分组数据报包 length具有偏移 ioffset指定主机上到指定的端口号。
DatagramPacket(byte[] buf, int length, SocketAddress address)
	构造用于发送长度的分组的数据报包 length指定主机上到指定的端口号。

DatagramPacket实例对象的方法:

DatagramPacket dp = new DatagramPacket(receive_data,receive_data.length);
dp.getData();	//获得缓冲区数据,字节数据,需要new String()  转为字符串
dp.getLength()  //返回要发送的数据的长度或接收到的数据的长度
Modifier and Type Method and Description
InetAddress getAddress() 返回该数据报发送或接收数据报的计算机的 IP 地址。
byte[] getData() 返回数据缓冲区。
int getLength() 返回要发送的数据的长度或接收到的数据的长度。
int getOffset() 返回要发送的数据的偏移量或接收到的数据的偏移量。
int getPort() 返回发送数据报的远程主机上的端口号,或从中接收数据报的端口号。
SocketAddress getSocketAddress() 获取该数据包发送到或正在从其发送的远程主机的 SocketAddress(通常为 IP 地址+端口号)。
void setAddress(InetAddress iaddr) 设置该数据报发送到的机器的 IP 地址。
void setData(byte[] buf) 设置此数据包的数据缓冲区。
void setData(byte[] buf, int offset, int length) 设置此数据包的数据缓冲区。
void setLength(int length) 设置此数据包的长度。
void setPort(int iport) 设置发送此数据报的远程主机上的端口号。
void setSocketAddress(SocketAddress address) 设置该数据报发送到的远程主机的 SocketAddress(通常是 IP 地址+端口号)。

练习:

发送:

接收:

写入文本
服务端

客户端:

四、URL

五、发送/接收 Email

六、HTTP 编程

七、RMI 远程调用


文章作者: CoderXiong
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 CoderXiong !
  目录