3.1_16 JavaSE入门 P15 【网络编程】InetAddress类、UDP协议、TCP协议

相关链接



P15 【网络编程】InetAddress类、UDP协议、TCP协议


  • 今日要掌握的内容:
  • 1.【应用】面向网络编程概述
    • a. 【理解】能够独立阐述IP地址的作用
    • b. 【理解】能够阐述端口号的作用
    • c. 【理解】能够阐述协议的作用
  • 2.【理解】UDP协议
    • a. 【应用】能够独立完成UDP协议的发送端
    • b. 【应用】能够独立完成UDP协议的接收端
  • 3.【理解】TCP协议
    • a. 【应用】 能够独立完成TCP协议的发送端
    • b. 【应用】 能够独立完成TCP协议的接收端
    • c. 【应用】 能够独立完成TCP协议的案例

1 概述


1.1 网络协议(网络模型)


  通过计算机网络可以使多台计算机实现连接,位于同一个网络中的计算机在进行连接和通信时需要遵守一定的规则,这就好比在道路中行驶的汽车一定要遵守交通规则一样。在计算机网络中,这些连接和通信的规则被称为网络通信协议,它对数据的传输格式、传输速率、传输步骤等做了统一规定,通信双方必须同时遵守才能完成数据交换。

  网络通信协议(网络模型)有很多种,一般是指

  OSI七层参考模型(Open System Interconnection Reference Model开放式系统互联通信参考模型)

  其中TCP/IP五层参考模型(Transmission Control Protocal/Internet Protoal传输控制协议/英特网互联协议)

  其中,TCP/IP是一个包括TCP协议(Transmission Control Protocol)IP协议UDP(User Datagram Protocol)协议和其它一些协议的协议组,在学习具体协议之前首先了解一下TCP/IP协议组的层次结构。

  在进行数据传输时,要求发送的数据与收到的数据完全一样,这时,就需要在原有的数据上添加很多信息,以保证数据在传输过程中数据格式完全一致。每一层都会对数据进行封装。

  hello-->数据封装-->最终数据在物理层传输(1010101101010) --> 数据拆包-->hello


  • 网络通信的原理:

在这里插入图片描述


1.1.1 OSI参考模型

在这里插入图片描述

图1.1.1-1 OSI网络模型


1. OSI的来源

  OSI(Open System Interconnect),即开放式系统互联。 一般都叫OSI参考模型,是ISO(国际标准化组织)组织在1985年研究的网络互连模型。

  ISO为了更好的使网络应用更为普及,推出了OSI参考模型。其含义就是推荐所有公司使用这个规范来控制网络。这样所有公司都有相同的规范,就能互联了。


2. OSI七层模型的划分

  OSI定义了网络互连的七层框架(物理层、数据链路层、网络层、传输层、会话层、表示层、应用层),即ISO开放互连系统参考模型。如下图。

  每一层实现各自的功能和协议,并完成与相邻层的接口通信。OSI的服务定义详细说明了各层所提供的服务。某一层的服务就是该层及其下各层的一种能力,它通过接口提供给更高一层。各层所提供的服务与这些服务是怎么实现的无关。


3. 各层功能定义

  这里我们只对OSI各层进行功能上的大概阐述,不详细深究,因为每一层实际都是一个复杂的层。后面我也会根据个人方向展开部分层的深入学习。这里我们就大概了解一下。我们从最顶层——应用层 开始介绍。整个过程以公司A和公司B的一次商业报价单发送为例子进行讲解。


(1) 应用层

  OSI参考模型中最靠近用户的一层,是为计算机用户提供应用接口,也为用户直接提供各种网络服务。我们常见应用层的网络服务协议有:HTTP,HTTPS,FTP,POP3、SMTP等。

  实际公司A的老板就是我们所述的用户,而他要发送的商业报价单,就是应用层提供的一种网络服务,当然,老板也可以选择其他服务,比如说,发一份商业合同,发一份询价单,等等。


(2) 表示层

  表示层提供各种用于应用层数据的编码和转换功能,确保一个系统的应用层发送的数据能被另一个系统的应用层识别。如果必要,该层可提供一种标准表示形式,用于将计算机内部的多种数据格式转换成通信中采用的标准表示形式。数据压缩和加密也是表示层可提供的转换功能之一。

  由于公司A和公司B是不同国家的公司,他们之间的商定统一用英语作为交流的语言,所以此时表示层(公司的文秘),就是将应用层的传递信息转翻译成英语。同时为了防止别的公司看到,公司A的人也会对这份报价单做一些加密的处理。这就是表示的作用,将应用层的数据转换翻译等。


(3) 会话层

  会话层就是负责建立、管理和终止表示层实体之间的通信会话。该层的通信由不同设备中的应用程序之间的服务请求和响应组成。

  会话层的同事拿到表示层的同事转换后资料,(会话层的同事类似公司的外联部),会话层的同事那里可能会掌握本公司与其他好多公司的联系方式,这里公司就是实际传递过程中的实体。他们要管理本公司与外界好多公司的联系会话。当接收到表示层的数据后,会话层将会建立并记录本次会话,他首先要找到公司B的地址信息,然后将整份资料放进信封,并写上地址和联系方式。准备将资料寄出。等到确定公司B接收到此份报价单后,此次会话就算结束了,外联部的同事就会终止此次会话。


(4) 传输层

  传输层建立了主机端到端的链接,传输层的作用是为上层协议提供端到端的可靠和透明的数据传输服务,包括处理差错控制和流量控制等问题。该层向高层屏蔽了下层数据通信的细节,使高层用户看到的只是在两个传输实体间的一条主机到主机的、可由用户控制和设定的、可靠的数据通路。我们通常说的,TCP UDP就是在这一层。端口号既是这里的“端”。

  传输层就相当于公司中的负责快递邮件收发的人,公司自己的投递员,他们负责将上一层的要寄出的资料投递到快递公司或邮局。


(5) 网络层

  本层通过IP寻址来建立两个节点之间的连接,为源端的运输层送来的分组,选择合适的路由和交换节点,正确无误地按照地址传送给目的端的运输层。就是通常说的IP层。这一层就是我们经常说的IP协议层。IP协议是Internet的基础。

  网络层就相当于快递公司庞大的快递网络,全国不同的集散中心,比如说,从深圳发往北京的顺丰快递(陆运为例啊,空运好像直接就飞到北京了),首先要到顺丰的深圳集散中心,从深圳集散中心再送到武汉集散中心,从武汉集散中心再寄到北京顺义集散中心。这个每个集散中心,就相当于网络中的一个IP节点。


(6) 数据链路层

  将比特组合成字节,再将字节组合成帧,使用链路层地址 (以太网使用MAC地址)来访问介质,并进行差错检测。

  数据链路层又分为2个子层:逻辑链路控制子层(LLC)和媒体访问控制子层(MAC)。

  MAC子层处理CSMA/CD算法、数据出错校验、成帧等;LLC子层定义了一些字段使上次协议能共享数据链路层。 在实际使用中,LLC子层并非必需的。

   这个没找到合适的例子


(7) 物理层

  实际最终信号的传输是通过物理层实现的。通过物理介质传输比特流。规定了电平、速度和电缆针脚。常用设备有(各种物理设备)集线器、中继器、调制解调器、网线、双绞线、同轴电缆。这些都是物理层的传输介质。

  快递寄送过程中的交通工具,就相当于我们的物理层,例如汽车,火车,飞机,船。


4.通信特点:对等通信

  对等通信,为了使数据分组从源传送到目的地,源端OSI模型的每一层都必须与目的端的对等层进行通信,这种通信方式称为对等层通信。在每一层通信过程中,使用本层自己协议进行通信。


1.1.2 TCP/IP五层模型

在这里插入图片描述

图1.1.2-1 TCP/IP网络模型各层作用

在这里插入图片描述

图1.1.2-2 TCP/IP网络模型各层应用

  上图中,TCP/IP协议中的五层分别是应用层、传输层、网络层和链路层,每层分别负责不同的通信功能,接下来针对这五层进行详细地讲解。

  应用层:主要负责应用程序的协议,例如HTTP协议、FTP协议等。

  传输层:主要使网络程序进行通信,在进行网络通信时,可以采用TCP协议,也可以采用UDP协议。

  网络层:网络层是整个TCP/IP协议的核心,它主要用于将传输的数据进行分组,将分组数据发送到目标计算机或者网络。

  链路层:链路层是用于定义物理传输通道,通常是对某些网络连接设备的驱动协议,例如针对光纤、网线提供的驱动。

  物理层:物理层主要定义物理设备标准,如网线的接口类型、光纤的接口类型、各种传输介质的传输速率等。主要作用是将数据最终编码为用0、1标识的比特流,通过物理介质传输。 这一层的数据叫做比特。


1.2 Socket(套接字)


  网络编程也叫Socket编程套接字编程。不同设备之间的数据传输就是不同Socket之间的数据传输。

  用于描述IP地址和端口,是一个通信链的句柄。在Internet上的主机一般运行了多个服务软件,同时提供几种服务。每种服务都打开一个Socket,并绑定到一个端口上,不同的端口对应不同的服务。

  Socket就是为网络编程提供的一种机制;通信的两端都有Socket;网络通信其实就是Socket间的通信;数据在两个Socket间通过IO传输


1.3 网络通信三要素


  • IP地址:InetAddress,主机地址,网络中设备的标识,不易记忆,可用主机名。

  • 端口号进程地址,用于标识进程的逻辑地址,不同进程的标识

  • 传输协议通讯的规则,规范常见的协议:TCPUDP


1.3.1 IP地址 InetAddress

  • IP地址:InetAddress,主机地址,网络中设备的标识,不易记忆,可用主机名。

  • IPV4:32bit(位)/8=4byte(字节)
    • IP地址长度为32,即有2^32-1个地址
      • 11000000,10101000,00000001,00000001
    • 点分十进制表示法(每8位划分)
      • 192.168.1.1

  • IPV6:128bit(位)/8=16byte(字节)
    • IP地址长度为128,即有2^128-1个地址
      • 00000001,…共128位
    • 点分十六进制表示法(每8位划分)
      • 首选格式:2031:0000:130F:0000:0000:09C0:876A:130B%5
      • 压缩格式:2031:0:130F::9C0:876A:130B%5
    • 【%5】 : 这个表示该地址仅限于标号为5的网络接口(一般指网卡或者虚拟网卡)。而在其他网络接口这个地址是无效的
    • 【::】 :可以把连续的一段0压缩为“::”。但为保证地址解析的唯一性,地址中”::”只能出现一次

1.3.2 端口号

  • 端口号:进程地址,用于标识进程的逻辑地址,不同进程的标识

  • 物理端口 网卡口

  • 逻辑端口 (平常说的端口号指逻辑端口)
    • 每个网络程序都会至少有一个逻辑端口
    • 用于表示进程的逻辑地址,不同进程的标识
    • 有效端口:0~65535,其中0~1024系统使用或保留端口

1.3.3 传输协议

  • 传输协议:通讯的规则,规范

  • UDP协议
    • 将数据源和目标地址封装到数据包中,不需要建立连接;每个数据包的大小限制在64k;因无连接,是不可靠的协议;不需要建立连接,速度快

  • TCP协议
    • 建立连接,形成传输数据的通道;在连接中进行大数据量传输;通过三次握手完成连接,是可靠协议;必须建立连接,效率会稍低

1.4 IP地址和端口号


  要想使网络中的计算机能够进行通信,必须为每台计算机指定一个标识号,通过这个标识号来指定接受数据的计算机或者发送数据的计算机。

  在TCP/IP协议中,这个标识号就是IP地址,它可以唯一标识一台计算机,目前,IP地址广泛使用的版本是IPv4,它是由4个字节大小的二进制数来表示,如:00001010000000000000000000000001。由于二进制形式表示的IP地址非常不便记忆和处理,因此通常会将IP地址写成十进制的形式,每个字节用一个十进制数字(0-255)表示,数字间用符号“.”分开,如 “192.168.1.100”。

  随着计算机网络规模的不断扩大,对IP地址的需求也越来越多,IPV4这种用4个字节表示的IP地址面临枯竭,因此IPv6 便应运而生了,IPv6使用16个字节表示IP地址,它所拥有的地址容量约是IPv4的8×1028倍,达到2128个(算上全零的),这样就解决了网络地址资源数量不够的问题。

  通过IP地址可以连接到指定计算机,但如果想访问目标计算机中的某个应用程序,还需要指定端口号。在计算机中,不同的应用程序是通过端口号区分的。端口号是用两个字节(16位的二进制数)表示的,它的取值范围是065535,其中,01023之间的端口号用于一些知名的网络服务和应用,用户的普通应用程序需要使用1024以上的端口号,从而避免端口号被另外一个应用或服务所占用。

  接下来通过一个图例来描述IP地址和端口号的作用,如下图所示。

在这里插入图片描述

  从上图中可以清楚地看到,位于网络中一台计算机可以通过IP地址去访问另一台计算机,并通过端口号访问目标计算机中的某个应用程序。


1.5 InetAddress类


  了解了IP地址的作用,我们看学习下JDK中java.net提供了一个InetAdderss类【InetAddress子类分为(IPV4)Inet4Address和(IPV6)Inet6Address】,该类用于封装一个IP地址,并提供了一系列与IP地址相关的方法,下表中列出了InetAddress类的一些常用方法。

在这里插入图片描述

//a.在给定主机名的情况下确定主机的IP地址(也可以直接输入IP地址)
InetAddress address = InetAddress.getByName(String host);

//b.返回本地主机
InetAddress address = InetAddress.getLocalHost();

//c.获取此IP地址的主机名
address.getHostName();

//d.返回IP地址字符串(以文本表现形式)
address.getHostAddress();

  • 主机名有可能重复,但IP地址是唯一的

在这里插入图片描述


案例代码一InetAddress类

package com.groupies.base.day15;

import java.net.InetAddress;
import java.net.UnknownHostException;

/**
 * @author GroupiesM
 * @date 2021/05/25
 * @introduction InetAddress类
 *
 *  InetAddress:此类标识互联网协议(IP)地址
 *
 * //a.在给定主机名的情况下确定主机的IP地址(也可以直接输入IP地址)
 * InetAddress address = InetAddress.getByName(String host);
 *
 * //b.返回本地主机
 * InetAddress address = InetAddress.getLocalHost();
 *
 * //c.获取此IP地址的主机名
 * address.getHostName();
 *
 * //d.返回IP地址字符串(以文本表现形式);
 * address.getHostAddress();
 */
public class Demo1InetAddress {
    public static void main(String[] args) throws UnknownHostException {
        //a.在给定主机名的情况下确定主机的IP地址(也可以直接输入IP地址)
        InetAddress address = InetAddress.getByName("GroupiesM");
        //InetAddress address = InetAddress.getByName("10.28.75.38");
        System.out.println(address);

        //c.获取此IP地址的主机名
        String hostAddress = address.getHostAddress();
        System.out.println(hostAddress);//10.28.75.38  返回ip地址

        //d.返回IP地址字符串(以文本表现形式)
        String hostName = address.getHostName();
        System.out.println(hostName);//GroupiesM  返回主机名

        //b.返回本地主机
        InetAddress localHost = InetAddress.getLocalHost();
        System.out.println(localHost);//GroupiesM/10.28.75.38  返回主机名+ip
    }
}

2 UDP协议


   UDP是无连接通信协议,即在数据传输时,数据的发送端和接收端不建立逻辑连接。简单来说,当一台计算机向另外一台计算机发送数据时,发送端不会确认接收端是否存在,就会发出数据,同样接收端在收到数据时,也不会向发送端反馈是否收到数据。

  由于使用UDP协议消耗资源小,通信效率高,所以通常都会用于音频、视频和普通数据的传输例如视频会议都使用UDP协议,因为这种情况即使偶尔丢失一两个数据包,也不会对接收结果产生太大影响。

  但是在使用UDP协议传送数据时,由于UDP的面向无连接性,不能保证数据的完整性,因此在传输重要数据时不建议使用UDP协议。UDP的交换过程如下图所示。

在这里插入图片描述


2.1 DatagramPacket类 数据报


   前面介绍了UDP是一种面向无连接的协议,因此,在通信时发送端和接收端不用建立连接。UDP通信的过程就像是货运公司在两个码头间发送货物一样。在码头发送和接收货物时都需要使用集装箱来装载货物,UDP通信也是一样,发送和接收的数据也需要使用“集装箱”进行打包,为此JDK中提供了一个DatagramPacket类,该类的实例对象就相当于一个集装箱,用于封装UDP通信中发送或者接收的数据

   想要创建一个DatagramPacket对象,首先需要了解一下它的构造方法。在创建发送端和接收端的DatagramPacket对象时,使用的构造方法有所不同,接收端的构造方法只需要接收一个字节数组来存放接收到的数据,而发送端的构造方法不但要接收存放了发送数据的字节数组,还需要指定发送端IP地址和端口号。

   接下来根据API文档的内容,对DatagramPacket的构造方法进行逐一详细地讲解。


在这里插入图片描述

//构造 DatagramPacket,用来接收长度为 length 的数据包。
DatagramPacket(byte[] buf, int length) 

   使用该构造方法在创建DatagramPacket对象时,指定了封装数据的字节数组数据的大小,没有指定IP地址和端口号。很明显,这样的对象只能用于接收端,不能用于发送端。因为发送端一定要明确指出数据的目的地(ip地址和端口号),而接收端不需要明确知道数据的来源,只需要接收到数据即可


在这里插入图片描述

//构造数据报包,用来将长度为 length 的包发送到指定主机上的指定端口号。
DatagramPacket(byte[] buf, int length, InetAddress address, int port) 

byte[] buf = new byte[1024];
DatagramPacket datagramPacket = new DatagramPacket(buf, 5);

   使用该构造方法在创建DatagramPacket对象时,不仅指定了封装数据的字节数组和数据的大小,还指定了数据包的目标IP地址(addr)和端口号(port)。该对象通常用于发送端,因为在发送数据时必须指定接收端的IP地址和端口号,就好像发送货物的集装箱上面必须标明接收人的地址一样。

   上面我们讲解了DatagramPacket的构造方法,接下来对DatagramPacket类中的常用方法进行详细地讲解,如下表所示。

在这里插入图片描述

//返回某台机器的 IP 地址,此数据报将要发往该机器或者是从该机器接收到的。 
InetAddress address = datagramPacket.getAddress();

//返回数据缓冲区。 
byte[] data = datagramPacket.getData(); 

//返回将要发送或接收到的数据的长度。 
int length = datagramPacket.getLength();

//返回将要发送或接收到的数据的偏移量。      
int offset = datagramPacket.getOffset();  

2.2 DatagramSocket类 收发


   DatagramPacket数据包的作用就如同是“集装箱”,可以将发送端或者接收端的数据封装起来。然而运输货物只有“集装箱”是不够的,还需要有码头。在程序中需要实现通信只有DatagramPacket数据包也同样不行,为此JDK中提供的一个DatagramSocket类。DatagramSocket类的作用就类似于“码头”,使用这个类的实例对象就可以发送和接收DatagramPacket数据包,发送数据的过程如下图所示。

在这里插入图片描述

   在创建发送端和接收端的DatagramSocket对象时,使用的构造方法也有所不同,下面对DatagramSocket类中常用的构造方法进行讲解。

在这里插入图片描述

//构造数据报套接字并将其绑定到本地主机上任何可用的端口
DatagramSocket ds = new DatagramSocket();
ds.bind(new InetSocketAddress(int port))//将此 DatagramSocket 绑定到特定的地址和端口。

   该构造方法用于创建发送端的DatagramSocket对象,在创建DatagramSocket对象时,并没有指定端口号,此时,系统会分配一个没有被其它网络程序所使用的端口号。

在这里插入图片描述

//创建数据报套接字并将其绑定到本地主机上的指定端口
DatagramSocket ds = new DatagramSocket(int port);

   该构造方法既可用于创建接收端的DatagramSocket对象,又可以创建发送端的DatagramSocket对象,在创建接收端的DatagramSocket对象时,必须要指定一个端口号,这样就可以监听指定的端口。

   上面我们讲解了DatagramSocket的构造方法,接下来对DatagramSocket类中的常用方法进行详细地讲解。

在这里插入图片描述

//从此套接字接收数据报包
ds.receive(DatagramPacket p);
    
//从此套接字发送数据报包
ds.send(DatagramPacket p);

2.3 案例


2.3.1 数据收发案例

注意: 需要先启动监听端,再启动发送端,才可以接收到数据。


案例代码二UDP协议——数据发送端

package com.groupies.base.day15;

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

/**
 * @author GroupiesM
 * @date 2021/05/25
 * @introduction UDP协议——数据发送端
 *
 *  步骤:
 *      1.创建发送端 Socket 对象 => DatagramSocket ds
 *      2.创建数据报包并指定目标ip、端口号 => DatagramPacket dp
 *      3.发送数据报包 => ds.send(dp);
 *      4.释放资源 => ds.close();
 *
 *  DatagramSocket:此类表示用来发送和接收数据,基于UDP协议的
 *  DatagramSocket():创建Socket对象并随机分配端口号
 *  DatagramSocket(int port):创建Socket对象并指定端口号
 *
 *  DatagramSocket构造方法:ds
 *      //构造数据报套接字并将其绑定到本地主机上任何可用的端口
 *      DatagramSocket ds = new DatagramSocket();
 *
 *      //创建数据报套接字并将其绑定到本地主机上的指定端口
 *      DatagramSocket ds = new DatagramSocket(int port);
 *
 *  DatagramPacket构造方法:dp
 *      //构造 DatagramPacket,用来接收长度为 length 的数据包。
 *      DatagramPacket(byte[] buf, int length)
 *
 *      //构造数据报包,用来将长度为 length 的包发送到指定主机上的指定端口号。
 *      DatagramPacket(byte[] buf, int length, InetAddress address, int port)
 *
 */
public class Demo2UDPSend {
    public static void main(String[] args) throws IOException {
        //1.创建发送端 Socket 对象 => DatagramSocket ds
        DatagramSocket ds = new DatagramSocket();

        //2.创建数据报包并指定目标ip、端口号 => DatagramPacket dp
        /*
         * DatagramPacket:此类标识数据报包
         * 数据 byte[]
         * 设备的地址 ip
         * 进程的地址 端口号
         *
         */
        String s = "hello udp,I'm comming!";
        byte[] buf = s.getBytes();

        int length = buf.length;//发送数组中所有数据,所以直接获取数组长度

        //只要这里没有报 UnknownHostException 异常,连接就没问题
        InetAddress address = InetAddress.getByName("GroupiesM");//我的主机名是:GroupiesM 发送给当前设备

        int port = 8888;

        /* DatagramPacket(byte[] buf, int length, InetAddress address, int port)
         * 构造数据报包,用来将长度为 length 的包发送到指定主机上的指定端口号。*/
        DatagramPacket dp = new DatagramPacket(buf, length, address, port);
        /** 除了构造器,还可以用成员方法设置来指定发送信息
         * dp.setData(buf);
         * dp.setLength(buf.length);
         * dp.setAddress(address);
         * dp.setPort(8888);
         */

        //3.发送数据报包 => ds.send(dp);
        ds.send(dp);

        //4.释放资源 => ds.close();
        ds.close();
    }
}

案例代码二UDP协议——数据监听端

package com.groupies.base.day15;

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

/**
 * @author GroupiesM
 * @date 2021/05/25
 * @introduction UDP协议——数据监听端
 *
 *  步骤:
 *      1.创建接收端 Socket 对象并指定端口号 => DatagramSocket ds
 *      2.创建数据报包容器,用于接收数据 => DatagramPacket dp
 *      3.接收数据报包 => ds.receive(dp);
 *      4.解析数据报包 => InetAddress address = dp.getAddress();
 *      5.输出数据 => address.getHostAddress();
 *      6.释放资源 => ds.close();
 *
 *  DatagramSocket:此类表示用来发送和接收数据,基于UDP协议的
 *  DatagramSocket(int port):创建Socket对象并指定端口号
 *
 *  DatagramSocket构造方法:ds
 *      //构造数据报套接字并将其绑定到本地主机上任何可用的端口
 *      DatagramSocket ds = new DatagramSocket();
 *
 *      //创建数据报套接字并将其绑定到本地主机上的指定端口
 *      DatagramSocket ds = new DatagramSocket(int port);
 *
 *  DatagramPacket构造方法:dp
 *      //构造 DatagramPacket,用来接收长度为 length 的数据包。
 *      DatagramPacket(byte[] buf, int length)
 *
 *      //构造数据报包,用来将长度为 length 的包发送到指定主机上的指定端口号。
 *      DatagramPacket(byte[] buf, int length, InetAddress address, int port)
 *
 */
public class Demo2UDPReceive {
    public static void main(String[] args) throws IOException {
        // 1.创建接收端 Socket 对象并指定端口号 => DatagramSocket ds
        DatagramSocket ds = new DatagramSocket(8888);

        byte[] bys = new byte[1024];
        // 2.创建数据报包容器,用于接收数据 => DatagramPacket dp
        DatagramPacket dp = new DatagramPacket(bys, bys.length);//DatagramPacket(byte[] buf, int length)

        System.out.println("========1=======");
        //3.接收数据报包 => ds.receive(dp);
        ds.receive(dp);//监听端口,接收数据前在这里阻塞
        System.out.println("========2=======");

        //4.解析数据报包 => InetAddress address = dp.getAddress();
        //InetAddress getAddress() : 获取发送端的IP对象
        InetAddress address = dp.getAddress();
        //byte[] getData()  :获取接收到的数据,也可以直接使用创建包对象时的数组
        byte[] data = dp.getData();
        //int getLength()  :获取具体收到数据的长度
        int length = dp.getLength();

        //5.输出数据 => address.getHostAddress();
        System.out.println("sender ---> " + address.getHostAddress());//sender ---> 2.0.1.144

        //将字节数组转为String
        System.out.println(new String(data, 0, length));//hello udp,I'm comming!
        System.out.println(new String(bys, 0, length));//hello udp,I'm comming!
        /**
         *  new DatagramPacket(bys,bys.length);=>调用setData方法 => this.buf = buf;
         *  byte[] data = dp.getData(); => return buf;
         *  所以 byte[] data 和 bys 指向堆中同一对象
         */
        //引用对象==比较内存地址,返回结果为true,证明data和bys指向内存中同一地址
        System.out.println("data == bys: " + (data == bys));//data == bys: true

        //6.释放资源 => ds.close();
        ds.close();
    }
}

2.3.2 数据收发的注意事项

1. 端口号错误,数据可以正常发出,不会出现异常,但是收不到数据

2. 端口号占用异常:Exception in thread “main” java.net.BindException: Address already in use: Cannot bind

  某个端口号已经被绑定时,启动监听端监听这个端口就会出现 BindException异常


案例代码三UDP协议——数据发送端

package com.groupies.base.day15;

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

/**
 * @author GroupiesM
 * @date 2021/05/26
 * @introduction UDP协议——数据发送端
 *
 *  使用匿名对象创建Socket对象,数据报包对象
 *
 *  UDP协议收发数据的注意事项:
 *      端口号错误,数据可以正常发出,不会出现异常
 */
public class Demo3UDPSend {
    public static void main(String[] args) throws IOException {
        //创建发送端Socket对象(随机端口)
        DatagramSocket ds = new DatagramSocket();
        //创建包对象
        byte[] bys = "hello udp,I'm comming again".getBytes();
        DatagramPacket dp = new DatagramPacket(bys, bys.length, InetAddress.getByName("GroupiesM"), 9999);
        //发送数据
        ds.send(dp);
        //释放资源
        ds.close();
    }
}

案例代码三UDP协议——数据监听端

package com.groupies.base.day15;

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

/**
 * @author GroupiesM
 * @date 2021/05/26
 * @introduction UDP协议——数据监听端
 *
 *  使用匿名对象创建Socket对象,数据报包对象
 *
 *  UDP协议收发数据的注意事项:
 *      1.端口号错误,数据可以正常发出,不会出现异常,但是收不到数据
 *      2.端口号占用异常:Exception in thread "main" java.net.BindException: Address already in use: Cannot bind
 *          端口号已经被绑定时,启动监听端会出现这个异常
 */
public class Demo3UDPReceive {
    public static void main(String[] args) throws IOException {
        //创建接收端Socket对象
        DatagramSocket ds = new DatagramSocket(9999);
        //创建包对象
        byte[] bys = new byte[1024];
        DatagramPacket dp = new DatagramPacket(bys, bys.length);
        //接收数据
        ds.receive(dp);
        //解析数据
        //获取发送端IP对象
        InetAddress address = dp.getAddress();
        //获取数据  可以省略 //byte[] data = dp.getData();
        //获取数据的长度
        int length = dp.getLength();

        //数据数据
        System.out.println("sender ---> " + address.getHostName());
        System.out.println(new String(bys, 0, length));
        //释放资源
        ds.close();
    }
}

3 TCP协议


  TCP通信同UDP通信一样,都能实现两台计算机之间的通信,通信的两端都需要创建socket对象。

  区别在于,UDP中只有发送端和接收端,不区分客户端与服务器端,计算机之间可以任意地发送数据。

  而TCP通信是严格区分客户端与服务器端的,在通信时,必须先由客户端去连接服务器端才能实现通信,服务器端不可以主动连接客户端,并且服务器端程序需要事先启动,等待客户端的连接。

  在JDK中提供了两个类用于实现TCP程序,一个是ServerSocket类,用于表示服务器端,一个是Socket类,用于表示客户端

  通信时,首先创建代表服务器端的ServerSocket对象,该对象相当于开启一个服务,并等待客户端的连接,然后创建代表客户端的Socket对象向服务器端发出连接请求,服务器端响应请求,两者建立连接开始通信。


3.1 ServerSocket类 服务器


  通过前面的学习知道,在开发TCP程序时,首先需要创建服务器端程序。JDK的java.net包中提供了一个ServerSocket类,该类的实例对象可以实现一个服务器段的程序。通过查阅API文档可知,ServerSocket类提供了多种构造方法,接下来就对ServerSocket的构造方法进行逐一地讲解。

在这里插入图片描述

//创建绑定到特定端口的服务器套接字。
ServerSocket(int port) 

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

  接下来学习一下ServerSocket的常用方法,如表所示。

在这里插入图片描述

ServerSocket ss = new ServerSocket(port);

//侦听并接受到此套接字的连接。
accept() 
Socket s = ss.accept();

//返回此服务器套接字的本地地址。
getInetAddress() 
InetAddress address = ss.getInetAddress();

  ServerSocket对象负责监听某台计算机的某个端口号,在创建ServerSocket对象后,需要继续调用该对象的accept()方法,接收来自客户端的请求。当执行了accept()方法之后,服务器端程序会发生阻塞,直到客户端发出连接请求,accept()方法才会返回一个Scoket对象用于和客户端实现通信,程序才能继续向下执行。


3.2 Socket类 客户端


  讲解了ServerSocket对象可以实现服务端程序,但只实现服务器端程序还不能完成通信,此时还需要一个客户端程序与之交互,为此JDK提供了一个Socket类,用于实现TCP客户端程序。

  通过查阅API文档可知Socket类同样提供了多种构造方法,接下来就对Socket的常用构造方法进行详细讲解。

在这里插入图片描述

//创建一个流套接字并将其连接到指定主机上的指定端口号。
Socket(String host, int port) 
Socket s = new Socket("GroupiesM", 8888);

  使用该构造方法在创建Socket对象时,会根据参数去连接在指定地址和端口上运行的服务器程序,其中参数host接收的是一个字符串类型的IP地址。

在这里插入图片描述

//创建一个流套接字并将其连接到指定 IP 地址的指定端口号。
Socket(InetAddress address, int port) 
Socket s = new Socket(InetAddress.getByName("GroupiesM"), 8888);

  该方法在使用上与第二个构造方法类似,参数address用于接收一个InetAddress类型的对象,该对象用于封装一个IP地址。

  在以上Socket的构造方法中,最常用的是第一个构造方法。

  接下来学习一下Socket的常用方法,如表所示。

方法声明功能描述
int getPort()该方法返回一个int类型对象,该对象是Socket对象与服务器端连接的端口号
InetAddress getLocalAddress()该方法用于获取Socket对象绑定的本地IP地址,并将IP地址封装成InetAddress类型的对象返回
void close()该方法用于关闭Socket连接,结束本次通信。在关闭socket之前,应将与socket相关的所有的输入/输出流全部关闭,这是因为一个良好的程序应该在执行完毕时释放所有的资源
InputStream getInputStream()该方法返回一个InputStream类型的输入流对象,如果该对象是由服务器端的Socket返回,就用于读取客户端发送的数据,反之,用于读取服务器端发送的数据
OutputStream getOutputStream()该方法返回一个OutputStream类型的输出流对象,如果该对象是由服务器端的Socket返回,就用于向客户端发送数据,反之,用于向服务器端发送数据

  在Socket类的常用方法中,getInputStream()和getOutStream()方法分别用于获取输入流和输出流。当客户端和服务端建立连接后,数据是以IO流的形式进行交互的,从而实现通信。

  接下来通过一张图来描述服务器端和客户端的数据传输,如下图所示。

在这里插入图片描述


3.3 案例


3.3.1 数据收发案例

案例代码四TCP协议——数据发送端(客户端Socket)

package com.groupies.base.day15;

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

/**
 * @author GroupiesM
 * @date 2021/05/27
 * @introduction TCP协议——数据发送端(客户端Socket)
 *
 *  使用TCP协议发送数据
 * 		1.创建发送端Socket对象(创建连接) => Socket s = new Socket(InetAddress.getByName("hostName"), port);
 * 		2.获取输出流对象 => OutputStream os = s.getOutputStream();
 * 		3.发送数据 => os.write("str".getBytes());
 * 		4.释放资源 => os.close();
 *
 * 	Socket(InetAddress address, int port)
 *
 * 	注意事项:
 * 	    先启动服务端,在启动客户端,否则会报出连接异常ConnectException
 * 	    Exception in thread "main" java.net.ConnectException: Connection refused: connect
 */
public class Demo4Client {
    public static void main(String[] args) throws IOException {
        //1.创建发送端Socket对象(创建连接) => Socket s = new Socket(InetAddress.getByName("hostName"), port);
        Socket s = new Socket(InetAddress.getByName("GroupiesM"), 10086);

        //2.获取输出流对象 => OutputStream os = s.getOutputStream();
        OutputStream os = s.getOutputStream();

        //3.发送数据
        os.write("hello tcp,I'm comming!!!".getBytes());

        //4.释放资源
        os.close();
        s.close();
    }
}

案例代码四TCP协议——数据接收端(服务器ServerSocket)

package com.groupies.base.day15;

import java.io.IOException;
import java.io.InputStream;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket;

/**
 * @author GroupiesM
 * @date 2021/05/27
 * @introduction TCP协议——数据接收端(服务器ServerSocket)
 *
 *  使用TCP协议接收数据
 *      1.创建接收端Socket对象 =>  ServerSocket ss = new ServerSocket(port);
 *      2.监听(阻塞) => Socket s = ss.accept();
 *      3.获取输入流对象 => InputStream is = s.getInputStream();
 *      4.获取数据 => byte[] bys = new byte[1024]; int len; len = is.read(bys);
 *      5.输出数据 => new String(bys, 0, len)
 *      6.释放资源 => is.close();
 *
 *  ServerSocket:接收端,服务端Socket
 *  ServerSocket(int port)
 *  Socket accept()
 */
public class Demo4Server {
    public static void main(String[] args) throws IOException {
        //1.创建接收端Socket对象 =>  ServerSocket ss = new ServerSocket(port);
        ServerSocket ss = new ServerSocket(10086);

        //2.监听(阻塞) => Socket s = ss.accept();
        Socket s = ss.accept();

        //3.获取输入流对象 => InputStream is = s.getInputStream();
        InputStream is = s.getInputStream();

        //4.获取数据 => byte[] bys = new byte[1024]; int len; len = is.read(bys);
        byte[] bys = new byte[1024];
        int len;//用于存储读到的字节个数
        len = is.read(bys);//将读到的输入流写入bys数组

        //5.输出数据 => new String(bys, 0, len)
        InetAddress address = s.getInetAddress();
        System.out.println("client ---> " + address.getHostName());//输出主机名
        System.out.println(new String(bys, 0, len));//数据数据

        //6.释放资源 => is.close();
        is.close();
        //ss.close(); //服务端一般不关闭
    }
}

3.3.2 数据接收转大写并返回案例

案例代码五TCP协议——数据发送端(客户端Socket)

package com.groupies.base.day15;

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

/**
 * @author GroupiesM
 * @date 2021/05/27
 * @introduction TCP协议--收发数据转换成大写返回(客户端Socket)
 *
 *  使用TCP协议发送数据
 * 		1.创建发送端Socket对象(创建连接) => Socket s = new Socket(InetAddress.getByName("hostName"), port);
 * 		2.获取输出流对象 => OutputStream os = s.getOutputStream();
 * 		3.发送数据 => os.write("str".getBytes());
 * 	    4.获取输入流对象 => InputStream is = s.getInputStream();
 * 	    5.获取数据 => byte[] bys = new byte[1024]; int len; len = is.read(bys);
 * 	    6.输出数据 => new String(bys, 0, len)
 * 		7.释放资源 => os.close();
 *
 * 收发数据流程:
 * 	    客户端发出数据
 * 	    ↓↓↓↓↓↓↓↓↓↓↓
 * 	    服务端接收数据
 * 	    ↓↓↓↓↓↓↓↓↓↓↓
 * 	    服务端转换数据
 * 	    ↓↓↓↓↓↓↓↓↓↓↓
 * 	    服务端发出数据
 * 	    ↓↓↓↓↓↓↓↓↓↓↓
 * 	    客户端接收数据
 *
 */
public class Demo5Client {
    public static void main(String[] args) throws IOException {
        //1.创建发送端Socket对象(创建连接) => Socket s = new Socket(InetAddress.getByName("hostName"), port);
        Socket s = new Socket(InetAddress.getByName("GroupiesM"), 8888);

        //2.获取输出流对象 => OutputStream os = s.getOutputStream();
        OutputStream os = s.getOutputStream();

        //3.发送数据 => os.write("str".getBytes());
        os.write("tcp,I'm comming again!!!".getBytes());

        //4.获取输入流对象 => InputStream is = s.getInputStream();
        InputStream is = s.getInputStream();

        //5.获取数据 => byte[] bys = new byte[1024]; int len; len = is.read(bys);
        byte[] bys = new byte[1024];
        int len = is.read(bys);

        //6.输出数据 => new String(bys, 0, len)
        System.out.println(new String(bys,0,len));

        //7.释放资源 => os.close();
        //os.close();
        s.close();
    }
}

案例代码五TCP协议——数据接收端(服务器ServerSocket)

package com.groupies.base.day15;

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

/**
 * @author GroupiesM
 * @date 2021/05/27
 * @introduction TCP协议--收发数据转换成大写返回(服务器ServerSocket)
 *
 *  使用TCP协议接收数据
 *      1.创建接收端Socket对象 =>  ServerSocket ss = new ServerSocket(port);
 *      2.监听(阻塞) => Socket s = ss.accept();
 *      3.获取输入流对象 => InputStream is = s.getInputStream();
 *      4.获取数据 => byte[] bys = new byte[1024]; int len; len = is.read(bys);
 *      5.输出数据 => new String(bys, 0, len)
 *      6.转换数据 => String upperStr = new String(bys, 0, len).toUpperCase();
 *      7.获取输出流对象 => OutputStream os = s.getOutputStream();
 *      8.返回数据(发出数据) => os.write(upperStr.getBytes());
 *      9.释放资源 => is.close();os.close();
 */
public class Demo5Server {
    public static void main(String[] args) throws IOException {
        //1.创建接收端Socket对象 =>  ServerSocket ss = new ServerSocket(port);
        ServerSocket ss = new ServerSocket(8888);
        
        //2.监听(阻塞) => Socket s = ss.accept();
        Socket s = ss.accept();
        
        //3.获取输入流对象 => InputStream is = s.getInputStream();
        InputStream is = s.getInputStream();
        
        //4.获取数据 => byte[] bys = new byte[1024]; int len; len = is.read(bys);
        byte[] bys = new byte[1024];
        int len = is.read(bys);
        
        //5.输出数据 => new String(bys, 0, len)
        System.out.println(new String(bys, 0, len));

        //6.转换数据
        String upperStr = new String(bys, 0, len).toUpperCase();

        //7.获取输出流对象
        OutputStream os = s.getOutputStream();

        //8.返回数据(发出数据)
        os.write(upperStr.getBytes());

        //9.释放资源
        is.close();
        os.close();
        //ss.close();//服务端一般不关闭
    }
}

3.3.3 模拟用户登录

需求:
  客户端输入用户名、密码,发送给服务端,校验后(zhangsan,123)返回登录成功/登陆失败


案例代码六模拟用户登录–客户端Socket

package com.groupies.base.day15;

import java.io.*;
import java.net.Socket;

/**
 * @author GroupiesM
 * @date 2021/05/27
 * @introduction 模拟用户登录--客户端Socket
 *
 *  需求:
 *      客户端输入用户名、密码,发送给服务端,校验后(zhangsan,123)返回登录成功/登陆失败
 *
 *  步骤:
 *      1.创建客户端Socket对象 => Socket s = new Socket("hostName", port);
 *      2.创建输入流对象(键盘)=> BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
 *      3.获取用户名和密码(InputStreamReader代替Scanner效率更高) => br.readLine();
 *      4.获取输出流对象(需要带换行功能) => PrintWriter out = new PrintWriter(s.getOutputStream(), true);
 *      5.写出数据 => out.println(username);out.println(password);
 *      6.获取输入流对象 => BufferedReader serverBr = new BufferedReader(new InputStreamReader(s.getInputStream()));
 *      7.获取服务器返回的数据 => serverBr.readLine();
 *      8.释放资源 => s.close();
 */
public class Demo6Client {
    public static void main(String[] args) throws IOException {
        //1.创建客户端Socket对象
        //Socket s = new Socket(InetAddress.getByName("GroupiesM"), 8888);
        Socket s = new Socket("GroupiesM", 8888);

        //2.创建输入流对象(键盘)
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));

        //3.获取用户名和密码
        System.out.println("请输入用户名");
        String username = br.readLine();
        System.out.println("请输入密码");
        String password = br.readLine();


        //4.获取输出流对象(需要带换行功能)
        OutputStream os = s.getOutputStream();
        //OutputStreamWriter osw = new OutputStreamWriter(os);
        //BufferedWriter bw = new BufferedWriter(osw);
        PrintWriter out = new PrintWriter(os, true);//自动换行 autoFlush:true

        //5.写出数据
        out.println(username);
        out.println(password);

        //6.获取输入流对象
        BufferedReader serverBr = new BufferedReader(new InputStreamReader(s.getInputStream()));

        //7.获取服务器返回的数据
        String result = serverBr.readLine();
        System.out.println(result);

        //8.释放资源
        s.close();
    }
}

案例代码六模拟用户登录–服务器ServerSocket

package com.groupies.base.day15;

import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;

/**
 * @author GroupiesM
 * @date 2021/05/27
 * @introduction 模拟用户登录--服务器ServerSocket
 *
 *  使用TCP协议接收数据
 *      1.创建接收端Socket对象 =>  ServerSocket ss = new ServerSocket(port);
 *      2.监听(阻塞) => Socket s = ss.accept();
 *      3.获取输入流对象(读一行) => BufferedReader br = new BufferedReader(new InputStreamReader(s.getInputStream()));
 *      4.获取数据(用户名和密码) => br.readLine();
 *      5.判断用户名和密码是否正确 => username.equals("zhangsan") && password.equals("123")
 *      6.获取输出流对象 => PrintWriter out = new PrintWriter(serverOs,true);
 *      7.返回判断结果 => out.println("登陆成功");
 *      8.释放资源 => is.close();os.close();
 *
 *  注意事项:
 *      7.获取输出流对象 如果没有开启自动刷新,需要手动flush或close触发刷新动作
 */
public class Demo6Server {
    public static void main(String[] args) throws IOException {
        //1.创建接收端Socket对象 =>  ServerSocket ss = new ServerSocket(port);
        ServerSocket ss = new ServerSocket(8888);

        //2.监听(阻塞) => Socket s = ss.accept();
        Socket s = ss.accept();

        //3.获取输入流对象 => InputStream is = s.getInputStream();
        InputStream is = s.getInputStream();
        InputStreamReader isr = new InputStreamReader(is);
        BufferedReader br = new BufferedReader(isr);

        //4.获取数据(用户名和密码) =>
        String username = br.readLine();//用户名
        String password = br.readLine();//密码


        //5.判断用户名和密码是否正确 =>
        boolean flag = false;
        if (username.equals("zhangsan") && password.equals("123")) {
            flag = true;
        }

        //6.获取输出流对象
        //OutputStream os = s.getOutputStream();
        OutputStream serverOs = s.getOutputStream();
        PrintWriter out = new PrintWriter(serverOs,true);

        //7.返回判断结果 =>
        if (flag){
            out.println("登陆成功");
        }else {
            out.println("登陆失败");
        }

        //8.释放资源
        s.close();
        //ss.close();//服务器一般不关闭
    }
}

3.3.4 模拟用户登录改写

需求:
  1.客户端输入用户名、密码,发送给服务端,校验后返回登录成功/登陆失败

  2.校验时,模拟从数据库中查询用户信息


案例代码七模拟用户登录改写–用户类User

package com.groupies.base.day15;

import java.util.Objects;

/**
 * @author GroupiesM
 * @date 2021/05/27
 * @introduction 模拟用户登录改写--用户类
 */
public class Demo7User {
    private String username;
    private String password;

    public Demo7User(String username, String password) {
        this.username = username;
        this.password = password;
    }

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Demo7User demo7User = (Demo7User) o;
        return Objects.equals(username, demo7User.username) && Objects.equals(password, demo7User.password);
    }
}

案例代码七模拟用户登录改写–模拟数据库UserDB

package com.groupies.base.day15;

import java.util.ArrayList;
import java.util.List;

/**
 * @author GroupiesM
 * @date 2021/05/27
 * @introduction 模拟用户登录改写--模拟数据库
 *
 *  存储用户对象
 */
public class Demo7UserDB {
    private static List<Demo7User> users = new ArrayList<>();

    //静态代码块--存储用户信息
    static {
        users.add(new Demo7User("zhangsan", "123"));
        users.add(new Demo7User("lisi", "312"));
        users.add(new Demo7User("admin", "admin"));
    }
    
    //获取所有用户信息
    public static List<Demo7User> getUsers(){
        return users;
    }
}

案例代码七模拟用户登录改写–客户端Socket

package com.groupies.base.day15;

import java.io.*;
import java.net.Socket;

/**
 * @author GroupiesM
 * @date 2021/05/27
 * @introduction 模拟用户登录改写--客户端Socket
 *
 *  需求:
 *      客户端输入用户名、密码,发送给服务端,校验后(zhangsan,123)返回登录成功/登陆失败
 *
 *  步骤:
 *      1.创建客户端Socket对象 => Socket s = new Socket("hostName", port);
 *      2.创建输入流对象(键盘)=> BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
 *      3.获取用户名和密码(InputStreamReader代替Scanner效率更高) => br.readLine();
 *      4.获取输出流对象(需要带换行功能) => PrintWriter out = new PrintWriter(s.getOutputStream(), true);
 *      5.写出数据 => out.println(username);out.println(password);
 *      6.获取输入流对象 => BufferedReader serverBr = new BufferedReader(new InputStreamReader(s.getInputStream()));
 *      7.获取服务器返回的数据 => serverBr.readLine();
 *      8.释放资源 => s.close();
 */
public class Demo7Client {
    public static void main(String[] args) throws IOException {
        //1.创建客户端Socket对象
        //Socket s = new Socket(InetAddress.getByName("GroupiesM"), 8888);
        Socket s = new Socket("GroupiesM", 8888);

        //2.创建输入流对象(键盘)
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));

        //3.获取用户名和密码
        System.out.println("请输入用户名");
        String username = br.readLine();
        System.out.println("请输入密码");
        String password = br.readLine();


        //4.获取输出流对象(需要带换行功能)
        OutputStream os = s.getOutputStream();
        //OutputStreamWriter osw = new OutputStreamWriter(os);
        //BufferedWriter bw = new BufferedWriter(osw);
        PrintWriter out = new PrintWriter(os, true);//自动换行 autoFlush:true

        //5.写出数据
        out.println(username);
        out.println(password);

        //6.获取输入流对象
        BufferedReader serverBr = new BufferedReader(new InputStreamReader(s.getInputStream()));

        //7.获取服务器返回的数据
        String result = serverBr.readLine();
        System.out.println(result);

        //8.释放资源
        s.close();
    }
}

案例代码七模拟用户登录改写–服务器ServerSocket

package com.groupies.base.day15;

import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.List;

/**
 * @author GroupiesM
 * @date 2021/05/27
 * @introduction 模拟用户登录改写--服务器ServerSocket
 *
 *  使用TCP协议接收数据
 *      1.创建接收端Socket对象 =>  ServerSocket ss = new ServerSocket(port);
 *      2.监听(阻塞) => Socket s = ss.accept();
 *      3.获取输入流对象(读一行) => BufferedReader br = new BufferedReader(new InputStreamReader(s.getInputStream()));
 *      4.获取数据(用户名和密码) => br.readLine();
 *      5.判断用户名和密码是否正确 => username.equals("zhangsan") && password.equals("123")
 *      6.获取输出流对象 => PrintWriter out = new PrintWriter(serverOs,true);
 *      7.返回判断结果 => out.println("登陆成功");
 *      8.释放资源 => is.close();os.close();
 *
 *  注意事项:
 *      7.获取输出流对象 如果没有开启自动刷新,需要手动flush或close触发刷新动作
 */
public class Demo7Server {
    public static void main(String[] args) throws IOException {
        //1.创建接收端Socket对象 =>  ServerSocket ss = new ServerSocket(port);
        ServerSocket ss = new ServerSocket(8888);

        //2.监听(阻塞) => Socket s = ss.accept();
        Socket s = ss.accept();

        //3.获取输入流对象 => InputStream is = s.getInputStream();
        InputStream is = s.getInputStream();
        InputStreamReader isr = new InputStreamReader(is);
        BufferedReader br = new BufferedReader(isr);

        //4.获取数据(用户名和密码) =>
        String username = br.readLine();//用户名
        String password = br.readLine();//密码

        //5.判断用户名和密码是否正确 =>
        boolean flag = false;
/*        if (username.equals("zhangsan") && password.equals("123")) {
            flag = true;
        }*/
        List<Demo7User> userList = Demo7UserDB.getUsers();
        Demo7User user = new Demo7User(username, password);
        if (userList.contains(user)) {
            //匹配成功(需要重写User类equals方法)
            flag = true;
        }

        //7.获取输出流对象
        //OutputStream os = s.getOutputStream();
        OutputStream serverOs = s.getOutputStream();
        PrintWriter out = new PrintWriter(serverOs, true);

        //6.返回判断结果 =>
        if (flag) {
            out.println(username + ": 登陆成功");
        } else {
            out.println(username + ": 登陆失败");
        }

        //8.释放资源
        s.close();
        //ss.close();//服务器一般不关闭
    }
}

4 IO通信模型


4.1 同步、异步、阻塞、非阻塞


同步: 同步函数一般指调用函数后,等到函数功能实现再返回,期间一直霸占的CPU,等待期间同一个线程无法执行其他函数;

异步: 异步函数指调用函数后,不管函数功能是否实现,立马返回;通过回调函数等告知函数功能完成;

阻塞: 调用某些函数阻塞是因为函数功能没有实现,主动放弃CPU,让其他线程的得以执行;当功能实现后,函数返回;效率低,安全

非阻塞: 调用某些函数不会进入阻塞,无论实现与否,都会返回结果。效率高,不安全


阻塞IO和非阻塞IO两个概念是程序级别的,主要描述请求的IO资源有没有准备好。

阻塞/非阻塞: 同步/非同步


根据前提,IO资源有没有准备好,分为:

a.站在系统的角度看

  如果响应程序–>同步IO

  如果不响应程序,只响应一个标识,(例:10分钟后)待IO准备好你去拿–> 非同步

b.站在程序的角度看

  如果继续等待–>阻塞IO

  如果不等待,通过线程不断轮询,待准备好IO后操作–>非阻塞IO


  网络通信的本质是网络间的数据IO,任何形式的IO操作都需要系统内核的支持

在这里插入图片描述


在这里插入图片描述


4.2 四种常见IO模型


同步阻塞IO(Blocking IO): 即传统的IO模型,BIO。

同步非阻塞IO(Non-blocking IO): 默认创建的socket都是阻塞的,非阻塞IO要求socket被设置为NONBLOCK。注意这里所说的NIO并非Java的NIO(New IO)库。

IO多路复用(IO Multiplexing): 即经典的Reactor设计模式,有时也称为异步阻塞IO,Java中的Selector和Linux中的epoll都是这种模型。

异步IO(Asynchronous IO): 即经典的Proactor设计模式,也称为异步非阻塞IO。


4.2.1 同步阻塞模型(Blocking IO)

在这里插入图片描述

应用程序调用一个IO函数,导致应用程序阻塞,等待数据准备好。 如果数据没有准备好,一直等待


特点:

  即用户需要等待read将socket中的数据读取到buffer后,才继续处理接收的数据。整个IO请求的过程中,用户线程是被阻塞的,这导致用户在发起IO请求时,不能做任何事情,对CPU的资源利用率不够。

  当希望能够立即发送和接收数据,且处理的套接字数量比较少的情况下,使用阻塞模式来开发网络程序比较合适。

  阻塞模式套接字的不足表现为,在大量建立好的套接字线程之间进行通信时比较困难。当使用“生产者-消费者”模型开发网络程序时,为每个套接字都分别分配一个读线程、一个处理数据线程和一个用于同步的事件,那么这样无疑加大系统的开销。其最大的缺点是当希望同时处理大量套接字时,将无从下手,其扩展性很差。


代码描述如下:

{
read(socket, buffer);

process(buffer);
}

4.2.2 同步非阻塞IO(Non-blocking IO)

在这里插入图片描述


特点:

  即用户需要不断地调用read,尝试读取socket中的数据,直到读取成功后,才继续处理接收的数据。整个IO请求的过程中,虽然用户线程每次发起IO请求后可以立即返回,但是为了等到数据,仍需要不断地轮询、重复请求,消耗了大量的CPU的资源。一般很少直接使用这种模型,而是在其他IO模型中使用非阻塞IO这一特性。

  不像阻塞模型需要为每个socket创建一个线程,非阻塞只需要一个线程,但是会一直占用CPU。


代码描述如下:

{
while(read(socket, buffer) != SUCCESS);

process(buffer);
}

4.2.3 IO多路复用(IO Multiplexing)

在这里插入图片描述

图4.2.3-1 多路分离函数select


特点:

  如上图所示,用户首先将需要进行IO操作的socket添加到select中,然后阻塞等待select系统调用返回。当数据到达时,socket被激活,select函数返回。用户线程正式发起read请求,读取数据并继续执行。

  从流程上来看,使用select函数进行IO请求和同步阻塞模型没有太大的区别,甚至还多了添加监视socket,以及调用select函数的额外操作,效率更差。但是,使用select以后最大的优势是用户可以在一个线程内同时处理多个socket的IO请求。用户可以注册多个socket,然后不断地调用select读取被激活的socket,即可达到在同一个线程内同时处理多个IO请求的目的。而在同步阻塞模型中,必须通过多线程的方式才能达到这个目的。


代码描述如下:

  其中while循环前将socket添加到select监视中,然后在while内一直调用select获取被激活的socket,一旦socket可读,便调用read函数将socket中的数据读取出来。

{
select(socket);
	while(true) {
		sockets = select();
		for(socket in sockets) {
			if(can_read(socket)) {
				read(socket, buffer);
				process(buffer);
			}
		}
	}
}

  然而,使用select函数的优点并不仅限于此。虽然上述方式允许单线程内处理多个IO请求,但是每个IO请求的过程还是阻塞的(在select函数上阻塞),平均时间甚至比同步阻塞IO模型还要长。如果用户线程只注册自己感兴趣的socket或者IO请求,然后去做自己的事情,等到数据到来时再进行处理,则可以提高CPU的利用率。

  IO多路复用模型使用了Reactor设计模式实现了这一机制。

在这里插入图片描述

图4.2.3-2 Reactor设计模式

  如图4.2.3-2所示

  EventHandler抽象类表示IO事件处理器,它拥有IO文件句柄Handle(通过get_handle获取),以及对Handle的操作handle_event(读/写等)。继承于EventHandler的子类可以对事件处理器的行为进行定制。

  Reactor类用于管理EventHandler(注册、删除等),并使用handle_events实现事件循环,不断调用同步事件多路分离器(一般是内核)的多路分离函数select,只要某个文件句柄被激活(可读/写等),select就返回(阻塞),handle_events就会调用与文件句柄关联的事件处理器的handle_event进行相关操作。

在这里插入图片描述

图4.2.3-3 IO多路复用

  如图4.2.3-3所示

  通过Reactor的方式,可以将用户线程轮询IO操作状态的工作统一交给handle_events事件循环进行处理。用户线程注册事件处理器之后可以继续执行做其他的工作(异步),而Reactor线程负责调用内核的select函数检查socket状态。当有socket被激活时,则通知相应的用户线程(或执行用户线程的回调函数),执行handle_event进行数据读取、处理的工作。由于select函数是阻塞的,因此多路IO复用模型也被称为异步阻塞IO模型。注意,这里的所说的阻塞是指select函数执行时线程被阻塞,而不是指socket。

  一般在使用IO多路复用模型时,socket都是设置为NONBLOCK的,不过这并不会产生影响,因为用户发起IO请求时,数据已经到达了,用户线程一定不会被阻塞。

  用户线程使用IO多路复用模型的伪代码描述为:

void UserEventHandler::handle_event() {
	if(can_read(socket)) {
		read(socket, buffer);
		process(buffer);
	}
}

{
Reactor.register(new UserEventHandler(socket));
}

  用户需要重写EventHandler的handle_event函数进行读取数据、处理数据的工作,用户线程只需要将自己的EventHandler注册到Reactor即可。Reactor中handle_events事件循环的伪代码大致如下。

Reactor::handle_events() {
	while(1) {
		sockets = select();
		for(socket in sockets) {
			get_event_handler(socket).handle_event();
		}
	}
}

  事件循环不断地调用select获取被激活的socket,然后根据获取socket对应的EventHandler,执行器handle_event函数即可。

  IO多路复用是最常使用的IO模型,但是其异步程度还不够“彻底”,因为它使用了会阻塞线程的select系统调用。因此IO多路复用只能称为异步阻塞IO,而非真正的异步IO。


4.2.4

异步IO(Asynchronous IO)

=>


5 RPC


RPC协议: 远程过程调用协议 (duboo),hadoop底层使用的就是Hadoop–RPC协议

  通俗的描述是: 客户端在不知道调用细节的情况下,调用存在于远程计算机上的某个过程或函数

  正式的描述是: 一种通过网络从远程计算机程序上请求服务,而不需要了解底层网络技术的协议


5.1 非RPC协议


在这里插入图片描述


5.2 RPC协议


在这里插入图片描述

在这里插入图片描述

Hadoop: HadoopRPC

spark : AKKA netty


6 面试题


/*
	面试题:Writer,字节缓冲流(BufferedInputStream),字符缓冲流(BufferedReader)的默认的缓冲区大小是多少?
			2KB		8KB								16KB
			
*/

Writer类
1024个char[] writeBuffer,每个char字符占2字节
1024 * 2 = 2048 byte = 2kb
在这里插入图片描述

字节缓冲流(BufferedInputStream/BufferedOutputStream)
8192 个 byte buf[],每个byte类型占1字节
8192 * 1 = 8192 byte = 8kb
在这里插入图片描述

字符缓冲流(BufferedReader/BufferedWriter)
8192 个 char cb[],每个char类型占2字节
8192 * 2 = 16384 byte = 16kb
在这里插入图片描述


7 扩展资料


TCP、UDP数据包大小的限制


21/05/27

M

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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值